refactor(parser): prune unused bit
This commit is contained in:
parent
5fc41b8e5e
commit
4a877cdc34
|
@ -19,6 +19,7 @@
|
||||||
"typings": "dist/index.d.ts",
|
"typings": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup --sourcemap",
|
"build": "tsup --sourcemap",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -36,9 +37,7 @@
|
||||||
"@babel/plugin-syntax-typescript": "^7.23.3",
|
"@babel/plugin-syntax-typescript": "^7.23.3",
|
||||||
"@babel/traverse": "^7.24.1",
|
"@babel/traverse": "^7.24.1",
|
||||||
"@babel/types": "^7.24.0",
|
"@babel/types": "^7.24.0",
|
||||||
"@openinula/class-transformer": "workspace:*",
|
|
||||||
"@openinula/reactivity-parser": "workspace:*",
|
"@openinula/reactivity-parser": "workspace:*",
|
||||||
"@openinula/view-generator": "workspace:*",
|
|
||||||
"@openinula/view-parser": "workspace:*",
|
"@openinula/view-parser": "workspace:*",
|
||||||
"@types/babel-types": "^7.0.15",
|
"@types/babel-types": "^7.0.15",
|
||||||
"@types/babel__generator": "^7.6.8",
|
"@types/babel__generator": "^7.6.8",
|
||||||
|
|
|
@ -14,16 +14,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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 { types as t } from '@openinula/babel-api';
|
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, getFnBodyPath } from '../utils';
|
import { extractFnFromMacro, getFnBodyPath } from '../../utils';
|
||||||
import { getDependenciesFromNode } from './reactive/getDependencies';
|
import { getDependenciesFromNode } from '@openinula/reactivity-parser';
|
||||||
|
import { reactivityFuncNames } from '../../const';
|
||||||
|
|
||||||
function isLifeCycleName(name: string): name is LifeCycle {
|
function isLifeCycleName(name: string): name is LifeCycle {
|
||||||
return [WILL_MOUNT, ON_MOUNT, WILL_UNMOUNT, ON_UNMOUNT].includes(name);
|
return [WILL_MOUNT, ON_MOUNT, WILL_UNMOUNT, ON_UNMOUNT].includes(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analyze the functional macro in the function component
|
* Analyze the functional macro in the function component
|
||||||
* 1. lifecycle
|
* 1. lifecycle
|
||||||
|
@ -50,12 +52,16 @@ export function functionalMacroAnalyze(): Visitor {
|
||||||
|
|
||||||
// watch
|
// watch
|
||||||
if (calleeName === WATCH) {
|
if (calleeName === WATCH) {
|
||||||
const fnNode = extractFnFromMacro(expression, WATCH);
|
const fnPath = extractFnFromMacro(expression, WATCH);
|
||||||
const depsPath = getWatchDeps(expression);
|
const depsPath = getWatchDeps(expression);
|
||||||
|
|
||||||
const [deps, depMask] = getDependenciesFromNode(depsPath ?? fnNode, ctx);
|
const dependency = getDependenciesFromNode(
|
||||||
|
(depsPath ?? fnPath).node,
|
||||||
|
ctx.current._reactiveBitMap,
|
||||||
|
reactivityFuncNames
|
||||||
|
);
|
||||||
|
|
||||||
addWatch(ctx.current, fnNode, deps, depMask);
|
addWatch(ctx.current, fnPath, dependency);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,14 +13,15 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { 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 NodePath } from '@babel/core';
|
import { type NodePath } from '@babel/core';
|
||||||
import { COMPONENT } from '../constants';
|
import { COMPONENT } from '../../constants';
|
||||||
import { analyzeFnComp } from '.';
|
import { analyzeFnComp } from '../index';
|
||||||
import { getDependenciesFromNode } from './reactive/getDependencies';
|
import { getDependenciesFromNode } from '@openinula/reactivity-parser';
|
||||||
import { types as t } from '@openinula/babel-api';
|
import { types as t } from '@openinula/babel-api';
|
||||||
|
import { reactivityFuncNames } from '../../const';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* collect all properties and methods from the node
|
* collect all properties and methods from the node
|
||||||
|
@ -43,7 +44,7 @@ export function variablesAnalyze(): Visitor {
|
||||||
} 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 depBits = 0;
|
let dependency;
|
||||||
if (isValidPath(init)) {
|
if (isValidPath(init)) {
|
||||||
// handle the method
|
// handle the method
|
||||||
if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
|
if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
|
||||||
|
@ -68,9 +69,9 @@ export function variablesAnalyze(): Visitor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
depBits = getDependenciesFromNode(init, ctx)[1];
|
dependency = getDependenciesFromNode(init.node, ctx.current._reactiveBitMap, reactivityFuncNames);
|
||||||
}
|
}
|
||||||
addProperty(ctx.current, id.node.name, init.node || null, depBits);
|
addProperty(ctx.current, id.node.name, init.node || null, dependency || null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
|
@ -13,13 +13,13 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Visitor } from './types';
|
import { Visitor } from '../types';
|
||||||
import { type NodePath } from '@babel/core';
|
import { type NodePath } from '@babel/core';
|
||||||
import { parseView as parseJSX } from '@openinula/jsx-view-parser';
|
import { parseView as parseJSX } from '@openinula/jsx-view-parser';
|
||||||
import { types as t, getBabelApi } from '@openinula/babel-api';
|
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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analyze the watch in the function component
|
* Analyze the watch in the function component
|
||||||
|
@ -34,15 +34,14 @@ export function viewAnalyze(): Visitor {
|
||||||
htmlTags,
|
htmlTags,
|
||||||
parseTemplate: false,
|
parseTemplate: false,
|
||||||
});
|
});
|
||||||
// @ts-expect-error TODO: FIX TYPE
|
|
||||||
const [viewParticles, usedPropertySet, usedBit] = parseReactivity(viewUnits, {
|
const [viewParticles, usedBit] = parseReactivity(viewUnits, {
|
||||||
babelApi: getBabelApi(),
|
babelApi: getBabelApi(),
|
||||||
availableProperties: current.availableVariables,
|
|
||||||
depMaskMap: current._reactiveBitMap,
|
depMaskMap: current._reactiveBitMap,
|
||||||
reactivityFuncNames,
|
reactivityFuncNames,
|
||||||
});
|
});
|
||||||
|
|
||||||
setViewChild(current, viewParticles, usedPropertySet, usedBit);
|
setViewChild(current, viewParticles, usedBit);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
|
@ -1,14 +1,13 @@
|
||||||
import { type NodePath } from '@babel/core';
|
import { type NodePath } from '@babel/core';
|
||||||
import { AnalyzeContext, Analyzer, Bitmap, ComponentNode, Visitor } from './types';
|
import { AnalyzeContext, Analyzer, ComponentNode, Visitor } from './types';
|
||||||
import { addLifecycle, createComponentNode } from './nodeFactory';
|
import { addLifecycle, createComponentNode } from './nodeFactory';
|
||||||
import { variablesAnalyze } from './variablesAnalyze';
|
import { variablesAnalyze } from './Analyzers/variablesAnalyze';
|
||||||
import { functionalMacroAnalyze } from './functionalMacroAnalyze';
|
import { functionalMacroAnalyze } from './Analyzers/functionalMacroAnalyze';
|
||||||
import { getFnBodyPath } from '../utils';
|
import { getFnBodyPath } from '../utils';
|
||||||
import { viewAnalyze } from './viewAnalyze';
|
import { viewAnalyze } from './Analyzers/viewAnalyze';
|
||||||
import { WILL_MOUNT } from '../constants';
|
import { WILL_MOUNT } from '../constants';
|
||||||
import { types as t } from '@openinula/babel-api';
|
import { types as t } from '@openinula/babel-api';
|
||||||
import { ViewParticle } from '@openinula/reactivity-parser';
|
import { pruneUnusedBit } from './pruneUnusedBit';
|
||||||
import { pruneComponentUnusedBit } from './pruneComponentUnusedBit';
|
|
||||||
|
|
||||||
const builtinAnalyzers = [variablesAnalyze, functionalMacroAnalyze, viewAnalyze];
|
const builtinAnalyzers = [variablesAnalyze, functionalMacroAnalyze, viewAnalyze];
|
||||||
|
|
||||||
|
@ -98,7 +97,7 @@ export function analyze(
|
||||||
const root = createComponentNode(fnName, path);
|
const root = createComponentNode(fnName, path);
|
||||||
analyzeFnComp(path, root, { analyzers, htmlTags: options.htmlTags });
|
analyzeFnComp(path, root, { analyzers, htmlTags: options.htmlTags });
|
||||||
|
|
||||||
pruneComponentUnusedBit(root);
|
pruneUnusedBit(root);
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
|
@ -14,9 +14,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
import { NodePath, type types as t } from '@babel/core';
|
||||||
import type { ComponentNode, FunctionalExpression, LifeCycle, ReactiveVariable, Bitmap } from './types';
|
import type { ComponentNode, FunctionalExpression, LifeCycle, ReactiveVariable, Dependency } from './types';
|
||||||
import { PropType } from '../constants';
|
import { Bitmap, ViewParticle } from '@openinula/reactivity-parser';
|
||||||
import { ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
|
|
||||||
export function createComponentNode(
|
export function createComponentNode(
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -30,7 +29,6 @@ export function createComponentNode(
|
||||||
children: undefined,
|
children: undefined,
|
||||||
variables: [],
|
variables: [],
|
||||||
usedBit: 0,
|
usedBit: 0,
|
||||||
usedPropertySet: parent ? new Set(parent.usedPropertySet) : new Set<string>(),
|
|
||||||
_reactiveBitMap: parent ? new Map<string, number>(parent._reactiveBitMap) : new Map<string, number>(),
|
_reactiveBitMap: parent ? new Map<string, number>(parent._reactiveBitMap) : new Map<string, number>(),
|
||||||
lifecycle: {},
|
lifecycle: {},
|
||||||
parent,
|
parent,
|
||||||
|
@ -48,17 +46,30 @@ export function createComponentNode(
|
||||||
return comp;
|
return comp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, depBits: number) {
|
export function addProperty(
|
||||||
|
comp: ComponentNode,
|
||||||
|
name: string,
|
||||||
|
value: t.Expression | null,
|
||||||
|
dependency: Dependency | null
|
||||||
|
) {
|
||||||
// The index of the variable in the availableVariables
|
// The index of the variable in the availableVariables
|
||||||
const idx = comp.availableVariables.length;
|
const idx = comp.availableVariables.length;
|
||||||
const bit = 1 << idx;
|
const bit = 1 << idx;
|
||||||
const bitmap = depBits ? depBits | bit : bit;
|
const fullDepBits = dependency?.fullDepMask;
|
||||||
|
const bitmap = fullDepBits ? fullDepBits | bit : bit;
|
||||||
|
|
||||||
if (depBits) {
|
if (fullDepBits) {
|
||||||
comp.usedBit |= depBits;
|
comp.usedBit |= fullDepBits;
|
||||||
}
|
}
|
||||||
comp._reactiveBitMap.set(name, bitmap);
|
comp._reactiveBitMap.set(name, bitmap);
|
||||||
comp.variables.push({ name, value, isComputed: !!depBits, type: 'reactive', depMask: bitmap, level: comp.level });
|
comp.variables.push({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
type: 'reactive',
|
||||||
|
_fullBits: bitmap,
|
||||||
|
level: comp.level,
|
||||||
|
dependency: dependency?.fullDepMask ? dependency : null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addMethod(comp: ComponentNode, name: string, value: FunctionalExpression) {
|
export function addMethod(comp: ComponentNode, name: string, value: FunctionalExpression) {
|
||||||
|
@ -81,21 +92,21 @@ export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: t
|
||||||
export function addWatch(
|
export function addWatch(
|
||||||
comp: ComponentNode,
|
comp: ComponentNode,
|
||||||
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>,
|
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>,
|
||||||
deps: Set<string>,
|
dependency: Dependency
|
||||||
usedBit: Bitmap
|
|
||||||
) {
|
) {
|
||||||
// if watch not exist, create a new one
|
// if watch not exist, create a new one
|
||||||
if (!comp.watch) {
|
if (!comp.watch) {
|
||||||
comp.watch = [];
|
comp.watch = [];
|
||||||
}
|
}
|
||||||
comp.usedPropertySet = new Set([...comp.usedPropertySet, ...deps]);
|
comp.usedBit |= dependency.fullDepMask;
|
||||||
comp.usedBit |= usedBit;
|
comp.watch.push({
|
||||||
comp.watch.push({ callback });
|
callback,
|
||||||
|
dependency: dependency.fullDepMask ? dependency : null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setViewChild(comp: ComponentNode, view: ViewParticle[], usedPropertySet: Set<string>, usedBit: Bitmap) {
|
export function setViewChild(comp: ComponentNode, view: ViewParticle[], usedBit: Bitmap) {
|
||||||
// TODO: Maybe we should merge
|
// TODO: Maybe we should merge
|
||||||
comp.usedPropertySet = usedPropertySet;
|
|
||||||
comp.usedBit |= usedBit;
|
comp.usedBit |= usedBit;
|
||||||
comp.children = view;
|
comp.children = view;
|
||||||
}
|
}
|
|
@ -13,56 +13,62 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Bitmap, ComponentNode } from './types';
|
import { ComponentNode } from './types';
|
||||||
import { ViewParticle } from '@openinula/reactivity-parser';
|
import { Bitmap, ViewParticle } from '@openinula/reactivity-parser';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To prune the bitmap of unused properties
|
* To prune the bitmap of unused properties
|
||||||
|
* Here the depMask will be defined by prune the unused bit in fullDepMask
|
||||||
* etc.:
|
* etc.:
|
||||||
* ```js
|
* ```js
|
||||||
* let a = 1; // 0b001
|
* let a = 1; // 0b001
|
||||||
* let b = 2; // 0b010 b is not used*, and should be pruned
|
* let b = 2; // 0b010 b is not used*, and should be pruned
|
||||||
* let c = 3; // 0b100 -> 0b010(cause bit of b is pruned)
|
* let c = 3; // 0b100 -> 0b010(cause bit of b is pruned)
|
||||||
* ```
|
* ```
|
||||||
* @param root
|
|
||||||
* @param index
|
|
||||||
*/
|
*/
|
||||||
export function pruneComponentUnusedBit(comp: ComponentNode<'comp'> | ComponentNode<'subComp'>, index = 1) {
|
export function pruneUnusedBit(
|
||||||
|
comp: ComponentNode<'comp'> | ComponentNode<'subComp'>,
|
||||||
|
index = 1,
|
||||||
|
bitPositionToRemoveInParent: number[] = []
|
||||||
|
) {
|
||||||
// dfs the component tree
|
// dfs the component tree
|
||||||
// To store the bitmap of the properties
|
// To store the bitmap of the properties
|
||||||
const bitMap = new Map<string, number>();
|
const bitMap = new Map<string, number>();
|
||||||
const bitPositionToRemove: number[] = [];
|
const bitPositionToRemove: number[] = [...bitPositionToRemoveInParent];
|
||||||
comp.variables.forEach(v => {
|
comp.variables.forEach(v => {
|
||||||
if (v.type === 'reactive') {
|
if (v.type === 'reactive') {
|
||||||
// get the origin bit, computed should keep the highest bit, etc. 0b0111 -> 0b0100
|
// get the origin bit, computed should keep the highest bit, etc. 0b0111 -> 0b0100
|
||||||
const originBit = keepHighestBit(v.depMask);
|
const originBit = keepHighestBit(v._fullBits);
|
||||||
if ((comp.usedBit & originBit) !== 0) {
|
|
||||||
v.bit = 1 << index;
|
if (comp.usedBit & originBit) {
|
||||||
|
v.bit = 1 << (index - bitPositionToRemove.length - 1);
|
||||||
bitMap.set(v.name, v.bit);
|
bitMap.set(v.name, v.bit);
|
||||||
if (v.isComputed) {
|
if (v.dependency) {
|
||||||
v.depMask = pruneBitmap(v.depMask, bitPositionToRemove);
|
// 去掉最高位
|
||||||
|
v.dependency.depMask = getDepMask(v.dependency.depBitmaps, bitPositionToRemove);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bitPositionToRemove.push(index);
|
bitPositionToRemove.push(index);
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
} else if (v.type === 'subComp') {
|
} else if (v.type === 'subComp') {
|
||||||
pruneComponentUnusedBit(v, index);
|
pruneUnusedBit(v, index, bitPositionToRemove);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
comp.watch?.forEach(watch => {
|
comp.watch?.forEach(watch => {
|
||||||
if (!watch.depMask) {
|
const dependency = watch.dependency;
|
||||||
|
if (!dependency) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
watch.depMask = pruneBitmap(watch.depMask, bitPositionToRemove);
|
dependency.depMask = getDepMask(dependency.depBitmaps, bitPositionToRemove);
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle children
|
// handle children
|
||||||
if (comp.children) {
|
if (comp.children) {
|
||||||
comp.children.forEach(child => {
|
comp.children.forEach(child => {
|
||||||
if (child.type === 'comp') {
|
if (child.type === 'comp') {
|
||||||
pruneComponentUnusedBit(child as ComponentNode<'comp'>, index);
|
pruneUnusedBit(child as ComponentNode<'comp'>, index, bitPositionToRemove);
|
||||||
} else {
|
} else {
|
||||||
pruneViewParticleUnusedBit(child as ViewParticle, bitPositionToRemove);
|
pruneViewParticleUnusedBit(child as ViewParticle, bitPositionToRemove);
|
||||||
}
|
}
|
||||||
|
@ -72,18 +78,25 @@ export function pruneComponentUnusedBit(comp: ComponentNode<'comp'> | ComponentN
|
||||||
|
|
||||||
function pruneBitmap(depMask: Bitmap, bitPositionToRemove: number[]) {
|
function pruneBitmap(depMask: Bitmap, bitPositionToRemove: number[]) {
|
||||||
// turn the bitmap to binary string
|
// turn the bitmap to binary string
|
||||||
const binary = depMask.toString(2);
|
const binaryStr = depMask.toString(2);
|
||||||
// remove the bit
|
const length = binaryStr.length;
|
||||||
binary
|
// iterate the binaryStr to keep the bit that is not in the bitPositionToRemove
|
||||||
.split('')
|
let result = '';
|
||||||
.reverse()
|
for (let i = length; i > 0; i--) {
|
||||||
.filter((bit, index) => {
|
if (!bitPositionToRemove.includes(i)) {
|
||||||
return !bitPositionToRemove.includes(index);
|
result = result + binaryStr[length - i];
|
||||||
})
|
}
|
||||||
.reverse()
|
}
|
||||||
.join('');
|
|
||||||
|
|
||||||
return parseInt(binary, 2);
|
return parseInt(result, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDepMask(depBitmaps: Bitmap[], bitPositionToRemove: number[]) {
|
||||||
|
// prune each dependency bitmap and combine them
|
||||||
|
return depBitmaps.reduce((acc, cur) => {
|
||||||
|
const a = pruneBitmap(cur, bitPositionToRemove);
|
||||||
|
return keepHighestBit(a) | acc;
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pruneViewParticleUnusedBit(particle: ViewParticle, bitPositionToRemove: number[]) {
|
function pruneViewParticleUnusedBit(particle: ViewParticle, bitPositionToRemove: number[]) {
|
||||||
|
@ -93,34 +106,31 @@ function pruneViewParticleUnusedBit(particle: ViewParticle, bitPositionToRemove:
|
||||||
const node = stack.pop()! as ViewParticle;
|
const node = stack.pop()! as ViewParticle;
|
||||||
if (node.type === 'template') {
|
if (node.type === 'template') {
|
||||||
node.props.forEach(prop => {
|
node.props.forEach(prop => {
|
||||||
prop.depMask = pruneBitmap(prop.depMask, bitPositionToRemove);
|
prop.depMask = getDepMask(prop.depBitmaps, bitPositionToRemove);
|
||||||
});
|
});
|
||||||
stack.push(node.template);
|
stack.push(node.template);
|
||||||
} else if (node.type === 'html') {
|
} else if (node.type === 'html') {
|
||||||
for (const key in node.props) {
|
for (const key in node.props) {
|
||||||
node.props[key].depMask = pruneBitmap(node.props[key].depMask, bitPositionToRemove);
|
node.props[key].depMask = getDepMask(node.props[key].depBitmaps, bitPositionToRemove);
|
||||||
}
|
}
|
||||||
stack.push(...node.children);
|
stack.push(...node.children);
|
||||||
} else if (node.type === 'text') {
|
} else if (node.type === 'text') {
|
||||||
node.content.depMask = pruneBitmap(node.content.depMask, bitPositionToRemove);
|
node.content.depMask = getDepMask(node.content.depBitmaps, bitPositionToRemove);
|
||||||
} else if (node.type === 'for') {
|
} else if (node.type === 'for') {
|
||||||
node.array.depMask = pruneBitmap(node.array.depMask, bitPositionToRemove);
|
node.array.depMask = getDepMask(node.array.depBitmaps, bitPositionToRemove);
|
||||||
stack.push(...node.children);
|
stack.push(...node.children);
|
||||||
} else if (node.type === 'if') {
|
} else if (node.type === 'if') {
|
||||||
node.branches.forEach(branch => {
|
node.branches.forEach(branch => {
|
||||||
branch.condition.depMask = pruneBitmap(branch.condition.depMask, bitPositionToRemove);
|
branch.condition.depMask = getDepMask(branch.condition.depBitmaps, bitPositionToRemove);
|
||||||
stack.push(...branch.children);
|
stack.push(...branch.children);
|
||||||
});
|
});
|
||||||
} else if (node.type === 'env') {
|
} else if (node.type === 'env') {
|
||||||
for (const key in node.props) {
|
for (const key in node.props) {
|
||||||
node.props[key].depMask = pruneBitmap(node.props[key].depMask, bitPositionToRemove);
|
node.props[key].depMask = getDepMask(node.props[key].depBitmaps, bitPositionToRemove);
|
||||||
}
|
}
|
||||||
stack.push(...node.children);
|
stack.push(...node.children);
|
||||||
} else if (node.type === 'exp') {
|
} else if (node.type === 'exp') {
|
||||||
node.content.depMask = pruneBitmap(node.content.depMask, bitPositionToRemove);
|
node.content.depMask = getDepMask(node.content.depBitmaps, bitPositionToRemove);
|
||||||
for (const key in node.props) {
|
|
||||||
node.props[key].depMask = pruneBitmap(node.props[key].depMask, bitPositionToRemove);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,20 +145,3 @@ function keepHighestBit(bitmap: number) {
|
||||||
// 使用按位与运算符只保留最高位
|
// 使用按位与运算符只保留最高位
|
||||||
return bitmap & mask;
|
return bitmap & mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeBit(bitmap: number, bitPosition: number) {
|
|
||||||
// 创建掩码,将目标位右边的位设置为 1,其他位设置为 0
|
|
||||||
const rightMask = (1 << (bitPosition - 1)) - 1;
|
|
||||||
|
|
||||||
// 创建掩码,将目标位左边的位设置为 1,其他位设置为 0
|
|
||||||
const leftMask = ~rightMask << 1;
|
|
||||||
|
|
||||||
// 提取右部分
|
|
||||||
const rightPart = bitmap & rightMask;
|
|
||||||
|
|
||||||
// 提取左部分并右移一位
|
|
||||||
const leftPart = (bitmap & leftMask) >> 1;
|
|
||||||
|
|
||||||
// 组合左部分和右部分
|
|
||||||
return leftPart | rightPart;
|
|
||||||
}
|
|
|
@ -15,11 +15,22 @@
|
||||||
|
|
||||||
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 { Bitmap, ViewParticle } 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;
|
||||||
export type Bitmap = number;
|
export type Dependency = {
|
||||||
|
dependenciesNode: t.ArrayExpression;
|
||||||
|
/**
|
||||||
|
* Only contains the bit of direct dependencies and not contains the bit of used variables
|
||||||
|
* So it's configured in pruneUnusedBit.ts
|
||||||
|
*/
|
||||||
|
depMask?: Bitmap;
|
||||||
|
/**
|
||||||
|
* The bitmap of each dependency
|
||||||
|
*/
|
||||||
|
depBitmaps: Bitmap[];
|
||||||
|
fullDepMask: Bitmap;
|
||||||
|
};
|
||||||
export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression;
|
export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression;
|
||||||
|
|
||||||
interface BaseVariable<V> {
|
interface BaseVariable<V> {
|
||||||
|
@ -32,16 +43,14 @@ export interface ReactiveVariable extends BaseVariable<t.Expression | null> {
|
||||||
level: number;
|
level: number;
|
||||||
bit?: Bitmap;
|
bit?: Bitmap;
|
||||||
/**
|
/**
|
||||||
* indicate the dependency of the variable | the index of the reactive variable
|
* Contains the bit of all dependencies graph
|
||||||
* i.e.
|
* i.e.
|
||||||
* let name = 'John'; // name's bitmap is 0x0001
|
* let name = 'John'; // name's _fullBits is 0x0001
|
||||||
* let age = 18; // age's bitmap is 0x0010
|
* let age = 18; // age's _fullBits is 0x0010
|
||||||
* let greeting = `Hello, ${name}`; // greeting's bitmap is 0x0101
|
* let greeting = `Hello, ${name}`; // greeting's _fullBits is 0x0101
|
||||||
*/
|
*/
|
||||||
depMask: Bitmap;
|
_fullBits: Bitmap;
|
||||||
// need a flag for computed to gen a getter
|
dependency: Dependency | null;
|
||||||
// watch is a static computed
|
|
||||||
isComputed: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MethodVariable extends BaseVariable<FunctionalExpression> {
|
export interface MethodVariable extends BaseVariable<FunctionalExpression> {
|
||||||
|
@ -67,10 +76,6 @@ export interface ComponentNode<Type = 'comp'> {
|
||||||
level: number;
|
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>;
|
|
||||||
usedBit: Bitmap;
|
usedBit: Bitmap;
|
||||||
/**
|
/**
|
||||||
* The map to find the reactive bitmap by name
|
* The map to find the reactive bitmap by name
|
||||||
|
@ -95,7 +100,7 @@ export interface ComponentNode<Type = 'comp'> {
|
||||||
* The watch fn in the component
|
* The watch fn in the component
|
||||||
*/
|
*/
|
||||||
watch?: {
|
watch?: {
|
||||||
depMask?: Bitmap;
|
dependency: Dependency | null;
|
||||||
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>;
|
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
|
@ -1,111 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { NodePath } from '@babel/core';
|
|
||||||
import { AnalyzeContext } from '../types';
|
|
||||||
import { types as t } from '@openinula/babel-api';
|
|
||||||
import { reactivityFuncNames } from '../../const';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get all valid dependencies of a babel path
|
|
||||||
* @param propertyKey
|
|
||||||
* @param path
|
|
||||||
* @param ctx
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function getDependenciesFromNode(
|
|
||||||
path: NodePath<t.Expression | t.ClassDeclaration>,
|
|
||||||
{ current }: AnalyzeContext
|
|
||||||
) {
|
|
||||||
// ---- Deps: console.log(count)
|
|
||||||
let depMask = 0;
|
|
||||||
// ---- Assign deps: count = 1 or count++
|
|
||||||
let assignDepMask = 0;
|
|
||||||
const depNodes: Record<string, t.Expression[]> = {};
|
|
||||||
const deps = new Set<string>();
|
|
||||||
|
|
||||||
const visitor = (innerPath: NodePath<t.Identifier>) => {
|
|
||||||
const propertyKey = innerPath.node.name;
|
|
||||||
const reactiveBitmap = current._reactiveBitMap.get(propertyKey);
|
|
||||||
|
|
||||||
if (reactiveBitmap !== undefined) {
|
|
||||||
if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) {
|
|
||||||
assignDepMask |= reactiveBitmap;
|
|
||||||
} else {
|
|
||||||
depMask |= reactiveBitmap;
|
|
||||||
deps.add(propertyKey);
|
|
||||||
|
|
||||||
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
|
|
||||||
if (assignDepMask & depMask) {
|
|
||||||
// TODO: I think we should throw an error here to indicate the user that there is a loop
|
|
||||||
}
|
|
||||||
|
|
||||||
return [deps, depMask] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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)
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { ViewParticle } from '@openinula/reactivity-parser';
|
||||||
|
import { ComponentNode } from '../analyze/types';
|
||||||
|
|
||||||
|
type Visitor = {
|
||||||
|
[Type in (ViewParticle | ComponentNode)['type']]: (
|
||||||
|
node: Extract<ViewParticle | ComponentNode, { type: Type }>,
|
||||||
|
ctx: any
|
||||||
|
) => void;
|
||||||
|
};
|
|
@ -1,94 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
|
@ -4,8 +4,6 @@ import syntaxTypescript from '@babel/plugin-syntax-typescript';
|
||||||
import inulaNext 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 { parse as babelParse } from '@babel/parser';
|
|
||||||
|
|
||||||
export default function (_: ConfigAPI, options: DLightOption): TransformOptions {
|
export default function (_: ConfigAPI, options: DLightOption): TransformOptions {
|
||||||
return {
|
return {
|
||||||
|
@ -13,25 +11,9 @@ export default function (_: ConfigAPI, options: DLightOption): TransformOptions
|
||||||
syntaxJSX.default ?? syntaxJSX,
|
syntaxJSX.default ?? syntaxJSX,
|
||||||
[syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }],
|
[syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }],
|
||||||
[syntaxDecorators.default ?? syntaxDecorators, { legacy: true }],
|
[syntaxDecorators.default ?? syntaxDecorators, { legacy: true }],
|
||||||
fn2Class,
|
|
||||||
[inulaNext, options],
|
[inulaNext, options],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type DLightOption };
|
export { type DLightOption };
|
||||||
|
|
||||||
export function parse(code: string) {
|
|
||||||
const result = babelParse(code, {
|
|
||||||
// parse in strict mode and allow module declarations
|
|
||||||
sourceType: 'module',
|
|
||||||
|
|
||||||
plugins: ['jsx'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.errors.length) {
|
|
||||||
throw new Error(result.errors[0].message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const program = result.program;
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type babel from '@babel/core';
|
||||||
import { NodePath, type PluginObj, type types as t } from '@babel/core';
|
import { NodePath, type PluginObj, type types as t } from '@babel/core';
|
||||||
import { type DLightOption } from './types';
|
import { type DLightOption } from './types';
|
||||||
import { defaultAttributeMap, defaultHTMLTags } from './const';
|
import { defaultAttributeMap, defaultHTMLTags } from './const';
|
||||||
import { analyze } from './analyzer';
|
import { analyze } from './analyze';
|
||||||
import { COMPONENT } from './constants';
|
import { COMPONENT } from './constants';
|
||||||
import { extractFnFromMacro, isCompPath } from './utils';
|
import { extractFnFromMacro, isCompPath } from './utils';
|
||||||
import { register } from '@openinula/babel-api';
|
import { register } from '@openinula/babel-api';
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
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/analyzer/functionalMacroAnalyze';
|
import { functionalMacroAnalyze } from '../../src/analyze/Analyzers/functionalMacroAnalyze';
|
||||||
import { types as t } from '@openinula/babel-api';
|
import { types as t } from '@openinula/babel-api';
|
||||||
|
|
||||||
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]);
|
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]);
|
||||||
|
|
|
@ -15,8 +15,9 @@
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { genCode, mockAnalyze } from '../mock';
|
import { genCode, mockAnalyze } from '../mock';
|
||||||
import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
|
import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze';
|
||||||
import { ReactiveVariable, SubCompVariable } from '../../src/analyzer/types';
|
import { ReactiveVariable, SubCompVariable } from '../../src/analyze/types';
|
||||||
|
import { findReactiveVarByName } from './utils';
|
||||||
|
|
||||||
const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze]);
|
const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze]);
|
||||||
|
|
||||||
|
@ -37,17 +38,18 @@ describe('analyze properties', () => {
|
||||||
Component(() => {
|
Component(() => {
|
||||||
let foo = 1;
|
let foo = 1;
|
||||||
let bar = foo;
|
let bar = foo;
|
||||||
|
let _ = bar; // use bar to avoid pruning
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.variables.length).toBe(2);
|
const fooVar = findReactiveVarByName(root, 'foo');
|
||||||
const fooVar = root.variables[0] as ReactiveVariable;
|
expect(!!fooVar.dependency).toBe(false);
|
||||||
expect(fooVar.isComputed).toBe(false);
|
|
||||||
expect(genCode(fooVar.value)).toBe('1');
|
expect(genCode(fooVar.value)).toBe('1');
|
||||||
|
|
||||||
const barVar = root.variables[1] as ReactiveVariable;
|
const barVar = findReactiveVarByName(root, 'bar');
|
||||||
expect(barVar.isComputed).toBe(true);
|
expect(!!barVar.dependency).toBe(true);
|
||||||
expect(genCode(barVar.value)).toBe('foo');
|
expect(genCode(barVar.value)).toBe('foo');
|
||||||
expect(barVar.depMask).toEqual(0b11);
|
expect(barVar.bit).toEqual(0b10);
|
||||||
|
expect(barVar.dependency!.depMask).toEqual(0b01);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should analyze dependency from state in different shape', () => {
|
it('should analyze dependency from state in different shape', () => {
|
||||||
|
@ -57,18 +59,18 @@ describe('analyze properties', () => {
|
||||||
let a = 1;
|
let a = 1;
|
||||||
let b = 0;
|
let b = 0;
|
||||||
let bar = { foo: foo ? a : b };
|
let bar = { foo: foo ? a : b };
|
||||||
|
let _ = bar; // use bar to avoid pruning
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.variables.length).toBe(4);
|
|
||||||
|
|
||||||
const barVar = root.variables[3] as ReactiveVariable;
|
const barVar = root.variables[3] as ReactiveVariable;
|
||||||
expect(barVar.isComputed).toBe(true);
|
expect(!!barVar.dependency).toBe(true);
|
||||||
expect(genCode(barVar.value)).toMatchInlineSnapshot(`
|
expect(genCode(barVar.value)).toMatchInlineSnapshot(`
|
||||||
"{
|
"{
|
||||||
foo: foo ? a : b
|
foo: foo ? a : b
|
||||||
}"
|
}"
|
||||||
`);
|
`);
|
||||||
expect(barVar.depMask).toEqual(0b1111);
|
expect(barVar.dependency!.depMask).toEqual(0b0111);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO:MOVE TO PROPS PLUGIN TEST
|
// TODO:MOVE TO PROPS PLUGIN TEST
|
||||||
|
@ -81,7 +83,7 @@ describe('analyze properties', () => {
|
||||||
expect(root.variables.length).toBe(1);
|
expect(root.variables.length).toBe(1);
|
||||||
|
|
||||||
const barVar = root.variables[0] as ReactiveVariable;
|
const barVar = root.variables[0] as ReactiveVariable;
|
||||||
expect(barVar.isComputed).toBe(true);
|
expect(!!barVar.dependency).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO:MOVE TO PROPS PLUGIN TEST
|
// TODO:MOVE TO PROPS PLUGIN TEST
|
||||||
|
@ -93,7 +95,7 @@ describe('analyze properties', () => {
|
||||||
`);
|
`);
|
||||||
expect(root.variables.length).toBe(1);
|
expect(root.variables.length).toBe(1);
|
||||||
const barVar = root.variables[0] as ReactiveVariable;
|
const barVar = root.variables[0] as ReactiveVariable;
|
||||||
expect(barVar.isComputed).toBe(true);
|
expect(!!barVar.dependency).toBe(true);
|
||||||
// @ts-expect-error ignore ts here
|
// @ts-expect-error ignore ts here
|
||||||
expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] });
|
expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] });
|
||||||
});
|
});
|
||||||
|
@ -103,12 +105,12 @@ describe('analyze properties', () => {
|
||||||
const cond = true
|
const cond = true
|
||||||
Component(() => {
|
Component(() => {
|
||||||
let bar = cond ? count : window.innerWidth;
|
let bar = cond ? count : window.innerWidth;
|
||||||
|
let _ = bar; // use bar to avoid pruning
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.variables.length).toBe(1);
|
|
||||||
const barVar = root.variables[0] as ReactiveVariable;
|
const barVar = root.variables[0] as ReactiveVariable;
|
||||||
expect(barVar.isComputed).toBe(false);
|
expect(!!barVar.dependency).toBe(false);
|
||||||
expect(barVar.depMask).toEqual(0b1);
|
expect(barVar.bit).toEqual(0b1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -119,16 +121,15 @@ describe('analyze properties', () => {
|
||||||
let foo = 1;
|
let foo = 1;
|
||||||
const Sub = Component(() => {
|
const Sub = Component(() => {
|
||||||
let bar = foo;
|
let bar = foo;
|
||||||
|
let _ = bar; // use bar to avoid pruning
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
expect(root.variables.length).toBe(2);
|
expect((root.variables[1] as SubCompVariable).ownAvailableVariables[0].dependency!.depMask).toBe(0b1);
|
||||||
expect(root.availableVariables[0].depMask).toEqual(0b1);
|
|
||||||
expect((root.variables[1] as SubCompVariable).ownAvailableVariables[0].depMask).toBe(0b11);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should analyze dependency in parent', () => {
|
it('should analyze dependency in parent', () => {
|
||||||
const root = analyze(`
|
const root = analyze(/*jsx*/ `
|
||||||
Component(() => {
|
Component(() => {
|
||||||
let lastName;
|
let lastName;
|
||||||
let parentFirstName = 'sheldon';
|
let parentFirstName = 'sheldon';
|
||||||
|
@ -137,19 +138,20 @@ describe('analyze properties', () => {
|
||||||
let middleName = parentName
|
let middleName = parentName
|
||||||
const name = 'shelly'+ middleName + lastName;
|
const name = 'shelly'+ middleName + lastName;
|
||||||
const GrandSon = Component(() => {
|
const GrandSon = Component(() => {
|
||||||
let grandSonName = 'bar' + lastName;
|
let grandSonName = name + lastName;
|
||||||
|
const _ = grandSonName; // use name to avoid pruning
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
`);
|
`);
|
||||||
const sonNode = root.variables[3] as SubCompVariable;
|
const sonNode = root.variables[3] as SubCompVariable;
|
||||||
// Son > middleName
|
// Son > middleName
|
||||||
expect(sonNode.ownAvailableVariables[0].depMask).toBe(0b1111);
|
expect(findReactiveVarByName(sonNode, 'middleName').dependency!.depMask).toBe(0b100);
|
||||||
// Son > name
|
// Son > name
|
||||||
expect(sonNode.ownAvailableVariables[1].depMask).toBe(0b11111);
|
expect(findReactiveVarByName(sonNode, 'name').dependency!.depMask).toBe(0b1001);
|
||||||
const grandSonNode = sonNode.variables[2] as SubCompVariable;
|
const grandSonNode = sonNode.variables[2] as SubCompVariable;
|
||||||
// GrandSon > grandSonName
|
// GrandSon > grandSonName
|
||||||
expect(grandSonNode.ownAvailableVariables[0].depMask).toBe(0b100001);
|
expect(grandSonNode.ownAvailableVariables[0].dependency!.depMask).toBe(0b10001);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -13,42 +13,52 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
|
import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze';
|
||||||
import { ComponentNode } from '../../src/analyzer/types';
|
import { viewAnalyze } from '../../src/analyze/Analyzers/viewAnalyze';
|
||||||
import { viewAnalyze } from '../../src/analyzer/viewAnalyze';
|
import { functionalMacroAnalyze } from '../../src/analyze/Analyzers/functionalMacroAnalyze';
|
||||||
import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze';
|
import { mockAnalyze } from '../mock';
|
||||||
import { genCode, mockAnalyze } from '../mock';
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { findReactiveVarByName, findSubCompByName } from './utils';
|
||||||
|
|
||||||
const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze, functionalMacroAnalyze]);
|
const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze, functionalMacroAnalyze]);
|
||||||
describe('prune unused bit', () => {
|
describe('prune unused bit', () => {
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
const root = analyze(/*js*/ `
|
const root = analyze(/*js*/ `
|
||||||
Component(({}) => {
|
Component(({}) => {
|
||||||
let name;
|
let unused0;
|
||||||
let className; // unused
|
let name; // 0b1
|
||||||
let className1; // unused
|
let unused;
|
||||||
let className2; // unused
|
let unused1;
|
||||||
let count = name; // 1
|
let unused2;
|
||||||
let doubleCount = count * 2; // 2
|
let count = name; // 0b10
|
||||||
|
let doubleCount = count * 2; // 0b100
|
||||||
const Input = Component(() => {
|
const Input = Component(() => {
|
||||||
let count3 = 1;
|
let count3 = 1;
|
||||||
let count2 = 1;
|
let count2 = 1;
|
||||||
let count = 1;
|
let count = 1; // 0b1000
|
||||||
return <input>{count}{doubleCount}</input>;
|
const db = count * 2; // 0b10000
|
||||||
|
return <input>{count}{db * count}</input>;
|
||||||
});
|
});
|
||||||
return <div className={count}>{doubleCount}</div>;
|
return <div className={count}>{doubleCount}</div>;
|
||||||
});
|
});
|
||||||
`);
|
`);
|
||||||
const div = root.children![0] as any;
|
// test computed
|
||||||
expect(div.children[0].content.depMask).toEqual(0b111);
|
const countVar = findReactiveVarByName(root, 'count');
|
||||||
expect(div.props.className.depMask).toEqual(0b11);
|
expect(countVar.bit).toEqual(0b10);
|
||||||
|
expect(countVar.dependency!.depMask).toEqual(0b1);
|
||||||
|
|
||||||
// @ts-expect-error ignore ts here
|
// test view
|
||||||
const InputCompNode = root.variables[4] as ComponentNode;
|
const div = root.children![0] as any;
|
||||||
// it's the {count}
|
expect(div.children[0].content.depMask).toEqual(0b100);
|
||||||
expect(inputFirstExp.content.depMask).toEqual(0b10000);
|
expect(div.props.className.depMask).toEqual(0b10);
|
||||||
// it's the {doubleCount}
|
|
||||||
expect(inputSecondExp.content.depMask).toEqual(0b1101);
|
// test sub component
|
||||||
|
const InputCompNode = findSubCompByName(root, 'Input');
|
||||||
|
// @ts-expect-error it's the {count}
|
||||||
|
const inputFirstExp = InputCompNode.children![0].children[0];
|
||||||
|
expect(inputFirstExp.content.depMask).toEqual(0b1000);
|
||||||
|
// @ts-expect-error it's the {doubleCount}
|
||||||
|
const inputSecondExp = InputCompNode.children![0].children![1];
|
||||||
|
expect(inputSecondExp.content.depMask).toEqual(0b11000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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 { ComponentNode, ReactiveVariable, SubCompVariable } from '../../src/analyze/types';
|
||||||
|
|
||||||
|
export function findReactiveVarByName(comp: ComponentNode | SubCompVariable, name: string): ReactiveVariable {
|
||||||
|
const result = comp.variables.find(v => v.name === name && v.type === 'reactive');
|
||||||
|
if (!result) {
|
||||||
|
throw new Error(`Can't find reactive variable ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result as ReactiveVariable;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findSubCompByName(comp: ComponentNode, name: string): SubCompVariable {
|
||||||
|
const result = comp.variables.find(v => v.name === name) as SubCompVariable;
|
||||||
|
if (!result) {
|
||||||
|
throw new Error(`Can't find subComp variable ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
|
import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze';
|
||||||
import { ComponentNode } from '../../src/analyzer/types';
|
import { ComponentNode } from '../../src/analyze/types';
|
||||||
import { viewAnalyze } from '../../src/analyzer/viewAnalyze';
|
import { viewAnalyze } from '../../src/analyze/Analyzers/viewAnalyze';
|
||||||
import { genCode, mockAnalyze } from '../mock';
|
import { genCode, mockAnalyze } from '../mock';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { findSubCompByName } from './utils';
|
||||||
|
|
||||||
const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze]);
|
const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze]);
|
||||||
describe('viewAnalyze', () => {
|
describe('viewAnalyze', () => {
|
||||||
|
@ -22,27 +23,22 @@ describe('viewAnalyze', () => {
|
||||||
});
|
});
|
||||||
`);
|
`);
|
||||||
const div = root.children![0] as any;
|
const div = root.children![0] as any;
|
||||||
expect(div.children[0].content.depMask).toEqual(0b11101);
|
expect(div.children[0].content.depMask).toEqual(0b10000);
|
||||||
expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount2]"');
|
expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount2]"');
|
||||||
expect(div.props.className.depMask).toEqual(0b111);
|
expect(div.props.className.depMask).toEqual(0b110);
|
||||||
expect(genCode(div.props.className.value)).toMatchInlineSnapshot('"className + count"');
|
expect(genCode(div.props.className.value)).toMatchInlineSnapshot('"className + count"');
|
||||||
|
|
||||||
|
const InputCompNode = findSubCompByName(root, 'Input');
|
||||||
// @ts-expect-error ignore ts here
|
// @ts-expect-error ignore ts here
|
||||||
const InputCompNode = root.variables[5] as ComponentNode;
|
|
||||||
expect(InputCompNode.usedPropertySet).toMatchInlineSnapshot(`
|
|
||||||
Set {
|
|
||||||
"count",
|
|
||||||
"doubleCount",
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
// it's the {count}
|
// it's the {count}
|
||||||
const inputFirstExp = InputCompNode.children![0].children[0];
|
const inputFirstExp = InputCompNode.children![0].children[0];
|
||||||
expect(inputFirstExp.content.depMask).toEqual(0b100000);
|
expect(inputFirstExp.content.depMask).toEqual(0b100000);
|
||||||
expect(genCode(inputFirstExp.content.dependenciesNode)).toMatchInlineSnapshot('"[count]"');
|
expect(genCode(inputFirstExp.content.dependenciesNode)).toMatchInlineSnapshot('"[count]"');
|
||||||
|
|
||||||
|
// @ts-expect-error ignore ts here
|
||||||
// it's the {doubleCount}
|
// it's the {doubleCount}
|
||||||
const inputSecondExp = InputCompNode.children[0].children[1];
|
const inputSecondExp = InputCompNode.children[0].children[1];
|
||||||
expect(inputSecondExp.content.depMask).toEqual(0b1101);
|
expect(inputSecondExp.content.depMask).toEqual(0b1000);
|
||||||
expect(genCode(inputSecondExp.content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount]"');
|
expect(genCode(inputSecondExp.content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount]"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze';
|
import { functionalMacroAnalyze } from '../../src/analyze/Analyzers/functionalMacroAnalyze';
|
||||||
import { genCode, mockAnalyze } from '../mock';
|
import { genCode, mockAnalyze } from '../mock';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
|
import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze';
|
||||||
|
|
||||||
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze, variablesAnalyze]);
|
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze, variablesAnalyze]);
|
||||||
|
|
||||||
|
@ -25,10 +25,10 @@ describe('watchAnalyze', () => {
|
||||||
console.log(a, b);
|
console.log(a, b);
|
||||||
}"
|
}"
|
||||||
`);
|
`);
|
||||||
if (!root.watch[0].depMask) {
|
if (!root.watch[0].dependency) {
|
||||||
throw new Error('watch deps not found');
|
throw new Error('watch deps not found');
|
||||||
}
|
}
|
||||||
expect(root.watch[0].depMask).toBe(0b11);
|
expect(root.watch[0].dependency.depMask).toBe(0b11);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should analyze watch expressions with dependency array', () => {
|
it('should analyze watch expressions with dependency array', () => {
|
||||||
|
@ -50,9 +50,9 @@ describe('watchAnalyze', () => {
|
||||||
// watch expression
|
// watch expression
|
||||||
}"
|
}"
|
||||||
`);
|
`);
|
||||||
if (!root.watch[0].depMask) {
|
if (!root.watch[0].dependency) {
|
||||||
throw new Error('watch deps not found');
|
throw new Error('watch deps not found');
|
||||||
}
|
}
|
||||||
expect(root.watch[0].depMask).toBe(0b11);
|
expect(root.watch[0].dependency.depMask).toBe(0b11);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Analyzer, ComponentNode } from '../src/analyzer/types';
|
import { Analyzer, ComponentNode } from '../src/analyze/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/analyzer';
|
import { analyze } from '../src/analyze';
|
||||||
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 '@openinula/babel-api';
|
import { register } from '@openinula/babel-api';
|
||||||
|
|
|
@ -2,12 +2,19 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"lib": ["ESNext", "DOM"],
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"esm": true
|
"esm": true
|
||||||
}
|
},
|
||||||
}
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
# babel-preset-inula-next
|
|
||||||
|
|
||||||
## 0.0.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies
|
|
||||||
- @openinula/class-transformer@0.0.2
|
|
||||||
|
|
||||||
## 0.0.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 2f9d373: feat: change babel import
|
|
|
@ -1,62 +0,0 @@
|
||||||
{
|
|
||||||
"name": "babel-preset-inula-next",
|
|
||||||
"version": "0.0.3",
|
|
||||||
"author": {
|
|
||||||
"name": "IanDx",
|
|
||||||
"email": "iandxssxx@gmail.com"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"dlight.js",
|
|
||||||
"babel-preset"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.cjs",
|
|
||||||
"module": "dist/index.js",
|
|
||||||
"typings": "dist/index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsup --sourcemap",
|
|
||||||
"test": "vitest"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/babel__core": "^7.20.5",
|
|
||||||
"@types/node": "^20.10.5",
|
|
||||||
"tsup": "^6.7.0",
|
|
||||||
"typescript": "^5.3.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/core": "^7.23.3",
|
|
||||||
"@babel/generator": "^7.23.6",
|
|
||||||
"@babel/parser": "^7.24.4",
|
|
||||||
"@babel/plugin-syntax-decorators": "^7.23.3",
|
|
||||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
|
||||||
"@babel/plugin-syntax-typescript": "^7.23.3",
|
|
||||||
"@babel/traverse": "^7.24.1",
|
|
||||||
"@babel/types": "^7.24.0",
|
|
||||||
"@openinula/class-transformer": "workspace:*",
|
|
||||||
"@openinula/reactivity-parser": "workspace:*",
|
|
||||||
"@openinula/view-generator": "workspace:*",
|
|
||||||
"@openinula/view-parser": "workspace:*",
|
|
||||||
"@types/babel-types": "^7.0.15",
|
|
||||||
"@types/babel__generator": "^7.6.8",
|
|
||||||
"@types/babel__parser": "^7.1.1",
|
|
||||||
"@types/babel__traverse": "^7.6.8",
|
|
||||||
"@openinula/jsx-view-parser": "workspace:*",
|
|
||||||
"minimatch": "^9.0.3",
|
|
||||||
"vitest": "^1.4.0"
|
|
||||||
},
|
|
||||||
"tsup": {
|
|
||||||
"entry": [
|
|
||||||
"src/index.ts"
|
|
||||||
],
|
|
||||||
"format": [
|
|
||||||
"cjs",
|
|
||||||
"esm"
|
|
||||||
],
|
|
||||||
"clean": true,
|
|
||||||
"dts": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
|
||||||
import { createComponentNode, createCondNode, createJSXNode } from './nodeFactory';
|
|
||||||
import { AnalyzeContext, Branch, Visitor } from './types';
|
|
||||||
import { isValidPath } from './utils';
|
|
||||||
|
|
||||||
export function earlyReturnAnalyze(): Visitor {
|
|
||||||
return {
|
|
||||||
ReturnStatement(path: NodePath<t.ReturnStatement>, context: AnalyzeContext) {
|
|
||||||
const currentComp = context.currentComponent;
|
|
||||||
|
|
||||||
const argument = path.get('argument');
|
|
||||||
if (argument.isJSXElement()) {
|
|
||||||
currentComp.child = createJSXNode(currentComp, argument);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
IfStatement(ifStmt: NodePath<t.IfStatement>, context: AnalyzeContext) {
|
|
||||||
if (!hasEarlyReturn(ifStmt)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentComp = context.currentComponent;
|
|
||||||
|
|
||||||
const branches: Branch[] = [];
|
|
||||||
let next: NodePath<t.Statement> | null = ifStmt;
|
|
||||||
let branchIdx = 0;
|
|
||||||
|
|
||||||
// Walk through the if-else chain to create branches
|
|
||||||
while (next && next.isIfStatement()) {
|
|
||||||
const nextConditions = [next.get('test')];
|
|
||||||
// gen id for branch with babel
|
|
||||||
const name = `$$branch-${branchIdx}`;
|
|
||||||
branches.push({
|
|
||||||
conditions: nextConditions,
|
|
||||||
content: createComponentNode(name, getStatements(ifStmt.get('consequent')), currentComp),
|
|
||||||
});
|
|
||||||
|
|
||||||
const elseBranch: NodePath<t.Statement | null | undefined> = next.get('alternate');
|
|
||||||
next = isValidPath(elseBranch) ? elseBranch : null;
|
|
||||||
branchIdx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time for the else branch
|
|
||||||
// We merge the else branch with the rest statements in fc body to form the children
|
|
||||||
const elseBranch = next ? getStatements(next) : [];
|
|
||||||
const defaultComponent = createComponentNode(
|
|
||||||
'$$branch-default',
|
|
||||||
elseBranch.concat(context.restStmt),
|
|
||||||
currentComp
|
|
||||||
);
|
|
||||||
context.skipRest();
|
|
||||||
|
|
||||||
currentComp.child = createCondNode(currentComp, defaultComponent, branches);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatements(next: NodePath<t.Statement>) {
|
|
||||||
return next.isBlockStatement() ? next.get('body') : [next];
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasEarlyReturn(path: NodePath<t.Node>) {
|
|
||||||
let hasReturn = false;
|
|
||||||
path.traverse({
|
|
||||||
ReturnStatement(path: NodePath<t.ReturnStatement>) {
|
|
||||||
if (
|
|
||||||
path.parentPath.isFunctionDeclaration() ||
|
|
||||||
path.parentPath.isFunctionExpression() ||
|
|
||||||
path.parentPath.isArrowFunctionExpression()
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hasReturn = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return hasReturn;
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
|
||||||
import { jsxSlicesAnalyze } from './jsxSliceAnalyze';
|
|
||||||
import { earlyReturnAnalyze } from './earlyReturnAnalyze';
|
|
||||||
import { AnalyzeContext, Analyzer, ComponentNode, CondNode, Visitor } from './types';
|
|
||||||
import { createComponentNode } from './nodeFactory';
|
|
||||||
import { propertiesAnalyze } from './propertiesAnalyze';
|
|
||||||
import { isValidComponent } from './utils';
|
|
||||||
|
|
||||||
const builtinAnalyzers = [jsxSlicesAnalyze, earlyReturnAnalyze, propertiesAnalyze];
|
|
||||||
let analyzers: Analyzer[] = builtinAnalyzers;
|
|
||||||
|
|
||||||
export function isCondNode(node: any): node is CondNode {
|
|
||||||
return node && node.type === 'cond';
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
|
||||||
return visitors.reduce((acc, cur) => {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
...cur(),
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// walk through the function component body
|
|
||||||
export function iterateFCBody(bodyStatements: NodePath<t.Statement>[], componentNode: ComponentNode, level = 0) {
|
|
||||||
const visitor = mergeVisitor(...analyzers);
|
|
||||||
const visit = (p: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
|
||||||
const type = p.node.type;
|
|
||||||
|
|
||||||
// TODO: More type safe way to handle this
|
|
||||||
visitor[type]?.(p as unknown as any, ctx);
|
|
||||||
};
|
|
||||||
for (let i = 0; i < bodyStatements.length; i++) {
|
|
||||||
const p = bodyStatements[i];
|
|
||||||
let skipRest = false;
|
|
||||||
const context: AnalyzeContext = {
|
|
||||||
level,
|
|
||||||
index: i,
|
|
||||||
currentComponent: componentNode,
|
|
||||||
restStmt: bodyStatements.slice(i + 1),
|
|
||||||
skipRest() {
|
|
||||||
skipRest = true;
|
|
||||||
},
|
|
||||||
traverse: (path: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
|
||||||
// @ts-expect-error TODO: fix visitor type incompatibility
|
|
||||||
path.traverse(visitor, ctx);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
visit(p, context);
|
|
||||||
|
|
||||||
if (p.isReturnStatement()) {
|
|
||||||
visitor.ReturnStatement?.(p, context);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skipRest) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The process of analyzing the component
|
|
||||||
* 1. identify the component
|
|
||||||
* 2. identify the jsx slice in the component
|
|
||||||
* 2. identify the component's props, including children, alias, and default value
|
|
||||||
* 3. analyze the early return of the component, build into the branch
|
|
||||||
*
|
|
||||||
* @param path
|
|
||||||
* @param customAnalyzers
|
|
||||||
*/
|
|
||||||
export function analyze(path: NodePath<t.FunctionDeclaration>, customAnalyzers?: Analyzer[]) {
|
|
||||||
const node = path.node;
|
|
||||||
if (!isValidComponent(node)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (customAnalyzers) {
|
|
||||||
analyzers = customAnalyzers;
|
|
||||||
}
|
|
||||||
const fnName = node.id.name;
|
|
||||||
const root = createComponentNode(fnName, path.get('body').get('body'));
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NodePath } from '@babel/core';
|
|
||||||
import { AnalyzeContext, Visitor } from './types';
|
|
||||||
import { createSubCompNode } from './nodeFactory';
|
|
||||||
import * as t from '@babel/types';
|
|
||||||
|
|
||||||
function genName(tagName: string, ctx: AnalyzeContext) {
|
|
||||||
return `$$${tagName}-Sub${ctx.currentComponent.subComponents.length}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function genNameFromJSX(path: NodePath<t.JSXElement>, ctx: AnalyzeContext) {
|
|
||||||
const tagId = path.get('openingElement').get('name');
|
|
||||||
if (tagId.isJSXIdentifier()) {
|
|
||||||
const jsxName = tagId.node.name;
|
|
||||||
return genName(jsxName, ctx);
|
|
||||||
}
|
|
||||||
throw new Error('JSXMemberExpression is not supported yet');
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceJSXSliceWithSubComp(name: string, ctx: AnalyzeContext, path: NodePath<t.JSXElement | t.JSXFragment>) {
|
|
||||||
// create a subComponent node and add it to the current component
|
|
||||||
const subComp = createSubCompNode(name, ctx.currentComponent, path.node);
|
|
||||||
ctx.currentComponent.subComponents.push(subComp);
|
|
||||||
|
|
||||||
// replace with the subComp jsxElement
|
|
||||||
const subCompJSX = t.jsxElement(
|
|
||||||
t.jsxOpeningElement(t.jsxIdentifier(name), [], true),
|
|
||||||
t.jsxClosingElement(t.jsxIdentifier(name)),
|
|
||||||
[],
|
|
||||||
true
|
|
||||||
);
|
|
||||||
path.replaceWith(subCompJSX);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyze the JSX slice in the function component
|
|
||||||
* 1. VariableDeclaration, like `const a = <div />`
|
|
||||||
* 2. SubComponent, like `function Sub() { return <div /> }`
|
|
||||||
*
|
|
||||||
* i.e.
|
|
||||||
* ```jsx
|
|
||||||
* let jsxSlice = <div>{count}</div>
|
|
||||||
* // =>
|
|
||||||
* function Comp_$id$() {
|
|
||||||
* return <div>{count}</div>
|
|
||||||
* }
|
|
||||||
* let jsxSlice = <Comp_$id$/>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function jsxSlicesAnalyze(): Visitor {
|
|
||||||
return {
|
|
||||||
JSXElement(path: NodePath<t.JSXElement>, ctx) {
|
|
||||||
const name = genNameFromJSX(path, ctx);
|
|
||||||
replaceJSXSliceWithSubComp(name, ctx, path);
|
|
||||||
path.skip();
|
|
||||||
},
|
|
||||||
JSXFragment(path: NodePath<t.JSXFragment>, ctx) {
|
|
||||||
replaceJSXSliceWithSubComp('frag', ctx, path);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
|
||||||
import { Branch, ComponentNode, CondNode, InulaNode, JSX, JSXNode, SubCompNode } from './types';
|
|
||||||
import { iterateFCBody } from './index';
|
|
||||||
|
|
||||||
export function createComponentNode(
|
|
||||||
name: string,
|
|
||||||
fnBody: NodePath<t.Statement>[],
|
|
||||||
parent?: ComponentNode
|
|
||||||
): ComponentNode {
|
|
||||||
const comp: ComponentNode = {
|
|
||||||
type: 'comp',
|
|
||||||
name,
|
|
||||||
props: {},
|
|
||||||
child: undefined,
|
|
||||||
subComponents: [],
|
|
||||||
methods: [],
|
|
||||||
state: [],
|
|
||||||
parent,
|
|
||||||
fnBody,
|
|
||||||
};
|
|
||||||
|
|
||||||
iterateFCBody(fnBody, comp);
|
|
||||||
|
|
||||||
return comp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addState(comp: ComponentNode, name: string, value: t.Expression | null) {
|
|
||||||
comp.state.push({ name, value });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addMethod(comp: ComponentNode, method: NodePath<t.FunctionDeclaration>) {
|
|
||||||
comp.methods.push(method);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createJSXNode(parent: ComponentNode, content: NodePath<JSX>): JSXNode {
|
|
||||||
return {
|
|
||||||
type: 'jsx',
|
|
||||||
parent,
|
|
||||||
child: content,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createCondNode(parent: ComponentNode, child: InulaNode, branches: Branch[]): CondNode {
|
|
||||||
return {
|
|
||||||
type: 'cond',
|
|
||||||
branches,
|
|
||||||
child,
|
|
||||||
parent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createSubCompNode(name: string, parent: ComponentNode, child: JSX): SubCompNode {
|
|
||||||
return {
|
|
||||||
type: 'subComp',
|
|
||||||
name,
|
|
||||||
parent,
|
|
||||||
child,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NodePath } from '@babel/core';
|
|
||||||
|
|
||||||
import { Visitor } from './types';
|
|
||||||
import { addMethod, addState } from './nodeFactory';
|
|
||||||
import { hasJSX, isValidComponentName, isValidPath } from './utils';
|
|
||||||
import { jsxSlicesAnalyze } from './jsxSliceAnalyze';
|
|
||||||
import * as t from '@babel/types';
|
|
||||||
|
|
||||||
// Analyze the JSX slice in the function component, including:
|
|
||||||
// 1. VariableDeclaration, like `const a = <div />`
|
|
||||||
// 2. SubComponent, like `function Sub() { return <div /> }`
|
|
||||||
function handleFn(fnName: string, fnBody: NodePath<t.BlockStatement>) {
|
|
||||||
if (isValidComponentName(fnName)) {
|
|
||||||
// This is a subcomponent, treat it as a normal component
|
|
||||||
} else {
|
|
||||||
// This is jsx creation function
|
|
||||||
// function jsxFunc() {
|
|
||||||
// // This is a function that returns JSX
|
|
||||||
// // because the function name is smallCamelCased
|
|
||||||
// return <div>{count}</div>
|
|
||||||
// }
|
|
||||||
// =>
|
|
||||||
// function jsxFunc() {
|
|
||||||
// function Comp_$id4$() {
|
|
||||||
// return <div>{count}</div>
|
|
||||||
// }
|
|
||||||
// // This is a function that returns JSX
|
|
||||||
// // because the function name is smallCamelCased
|
|
||||||
// return <Comp_$id4$/>
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. jsx creation function, like `function create() { return <div /> }`
|
|
||||||
export function propertiesAnalyze(): Visitor {
|
|
||||||
return {
|
|
||||||
VariableDeclaration(path: NodePath<t.VariableDeclaration>, ctx) {
|
|
||||||
const declarations = path.get('declarations');
|
|
||||||
// iterate the declarations
|
|
||||||
declarations.forEach(declaration => {
|
|
||||||
const id = declaration.get('id');
|
|
||||||
// handle destructuring
|
|
||||||
if (id.isObjectPattern()) {
|
|
||||||
throw new Error('Object destructuring is not supported yet');
|
|
||||||
} else if (id.isArrayPattern()) {
|
|
||||||
// TODO: handle array destructuring
|
|
||||||
throw new Error('Array destructuring is not supported yet');
|
|
||||||
} else if (id.isIdentifier()) {
|
|
||||||
const init = declaration.get('init');
|
|
||||||
if (isValidPath(init) && hasJSX(init)) {
|
|
||||||
if (init.isArrowFunctionExpression()) {
|
|
||||||
const fnName = id.node.name;
|
|
||||||
const fnBody = init.get('body');
|
|
||||||
|
|
||||||
// handle case like `const jsxFunc = () => <div />`
|
|
||||||
if (fnBody.isExpression()) {
|
|
||||||
// turn expression into block statement for consistency
|
|
||||||
fnBody.replaceWith(t.blockStatement([t.returnStatement(fnBody.node)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// We switched to the block statement above, so we can safely call handleFn
|
|
||||||
handleFn(fnName, fnBody as NodePath<t.BlockStatement>);
|
|
||||||
}
|
|
||||||
// handle jsx slice
|
|
||||||
ctx.traverse(path, ctx);
|
|
||||||
}
|
|
||||||
addState(ctx.currentComponent, id.node.name, declaration.node.init || null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
FunctionDeclaration(path: NodePath<t.FunctionDeclaration>, ctx) {
|
|
||||||
const fnId = path.node.id;
|
|
||||||
if (!fnId) {
|
|
||||||
// This is an anonymous function, collect into lifecycle
|
|
||||||
//TODO
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasJSX(path)) {
|
|
||||||
// This is a normal function, collect into methods
|
|
||||||
addMethod(ctx.currentComponent, path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFn(fnId.name, path.get('body'));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NodePath, types as t } from '@babel/core';
|
|
||||||
import { Node } from '@babel/traverse';
|
|
||||||
|
|
||||||
// --- Node shape ---
|
|
||||||
export type InulaNode = ComponentNode | CondNode | JSXNode;
|
|
||||||
export type JSX = t.JSXElement | t.JSXFragment;
|
|
||||||
|
|
||||||
type defaultVal = any | null;
|
|
||||||
type Bitmap = number;
|
|
||||||
interface Reactive {
|
|
||||||
name: string;
|
|
||||||
value: t.Expression | null;
|
|
||||||
// indicate the value is a state or computed or watch
|
|
||||||
listeners: string[];
|
|
||||||
bitmap: Bitmap;
|
|
||||||
// need a flag for computed to gen a getter
|
|
||||||
// watch is a static computed
|
|
||||||
isComputed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComponentNode {
|
|
||||||
type: 'comp';
|
|
||||||
name: string;
|
|
||||||
props: Record<string, defaultVal>;
|
|
||||||
// A valuable could be a state or computed
|
|
||||||
valuable: Reactive;
|
|
||||||
methods: NodePath<t.FunctionDeclaration>[];
|
|
||||||
child?: InulaNode;
|
|
||||||
subComponents: ComponentNode[];
|
|
||||||
parent?: ComponentNode;
|
|
||||||
/**
|
|
||||||
* The function body of the fn component code
|
|
||||||
*/
|
|
||||||
// fnBody: NodePath<t.Statement>[];
|
|
||||||
// a map to find the state
|
|
||||||
reactiveMap: Record<string, Bitmap>;
|
|
||||||
level: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubCompNode {
|
|
||||||
type: 'subComp';
|
|
||||||
name: string;
|
|
||||||
parent: ComponentNode;
|
|
||||||
child: JSX;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface JSXNode {
|
|
||||||
type: 'jsx';
|
|
||||||
parent: ComponentNode;
|
|
||||||
child: NodePath<JSX>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CondNode {
|
|
||||||
type: 'cond';
|
|
||||||
branches: Branch[];
|
|
||||||
parent: ComponentNode;
|
|
||||||
/**
|
|
||||||
* The default branch
|
|
||||||
*/
|
|
||||||
child: InulaNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Branch {
|
|
||||||
conditions: NodePath<t.Expression>[];
|
|
||||||
content: InulaNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AnalyzeContext {
|
|
||||||
level: number;
|
|
||||||
index: number;
|
|
||||||
currentComponent: ComponentNode;
|
|
||||||
restStmt: NodePath<t.Statement>[];
|
|
||||||
// --- flow control ---
|
|
||||||
/**
|
|
||||||
* ignore the rest of the statements
|
|
||||||
*/
|
|
||||||
skipRest: () => void;
|
|
||||||
traverse: (p: NodePath<t.Statement>, ctx: AnalyzeContext) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Visitor<S = AnalyzeContext> = {
|
|
||||||
[Type in Node['type']]?: (path: NodePath<Extract<Node, { type: Type }>>, state: S) => void;
|
|
||||||
};
|
|
||||||
export type Analyzer = () => Visitor;
|
|
||||||
|
|
||||||
export interface FnComponentDeclaration extends t.FunctionDeclaration {
|
|
||||||
id: t.Identifier;
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
|
||||||
import { FnComponentDeclaration } from './types';
|
|
||||||
|
|
||||||
export function isValidPath<T>(path: NodePath<T>): path is NodePath<Exclude<T, undefined | null>> {
|
|
||||||
return !!path.node;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The component name must be UpperCamelCase
|
|
||||||
export function isValidComponent(node: t.FunctionDeclaration): node is FnComponentDeclaration {
|
|
||||||
// the first letter of the component name must be uppercase
|
|
||||||
return node.id ? isValidComponentName(node.id.name) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidComponentName(name: string) {
|
|
||||||
// the first letter of the component name must be uppercase
|
|
||||||
return /^[A-Z]/.test(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasJSX(path: NodePath<t.Node>) {
|
|
||||||
if (path.isJSXElement()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if there is JSXElement in the children
|
|
||||||
let seen = false;
|
|
||||||
path.traverse({
|
|
||||||
JSXElement() {
|
|
||||||
seen = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return seen;
|
|
||||||
}
|
|
|
@ -1,490 +0,0 @@
|
||||||
export const devMode = process.env.NODE_ENV === 'development';
|
|
||||||
|
|
||||||
export const alterAttributeMap = {
|
|
||||||
class: 'className',
|
|
||||||
for: 'htmlFor',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const reactivityFuncNames = [
|
|
||||||
// ---- Array
|
|
||||||
'push',
|
|
||||||
'pop',
|
|
||||||
'shift',
|
|
||||||
'unshift',
|
|
||||||
'splice',
|
|
||||||
'sort',
|
|
||||||
'reverse',
|
|
||||||
// ---- Set
|
|
||||||
'add',
|
|
||||||
'delete',
|
|
||||||
'clear',
|
|
||||||
// ---- Map
|
|
||||||
'set',
|
|
||||||
'delete',
|
|
||||||
'clear',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const defaultHTMLTags = [
|
|
||||||
'a',
|
|
||||||
'abbr',
|
|
||||||
'address',
|
|
||||||
'area',
|
|
||||||
'article',
|
|
||||||
'aside',
|
|
||||||
'audio',
|
|
||||||
'b',
|
|
||||||
'base',
|
|
||||||
'bdi',
|
|
||||||
'bdo',
|
|
||||||
'blockquote',
|
|
||||||
'body',
|
|
||||||
'br',
|
|
||||||
'button',
|
|
||||||
'canvas',
|
|
||||||
'caption',
|
|
||||||
'cite',
|
|
||||||
'code',
|
|
||||||
'col',
|
|
||||||
'colgroup',
|
|
||||||
'data',
|
|
||||||
'datalist',
|
|
||||||
'dd',
|
|
||||||
'del',
|
|
||||||
'details',
|
|
||||||
'dfn',
|
|
||||||
'dialog',
|
|
||||||
'div',
|
|
||||||
'dl',
|
|
||||||
'dt',
|
|
||||||
'em',
|
|
||||||
'embed',
|
|
||||||
'fieldset',
|
|
||||||
'figcaption',
|
|
||||||
'figure',
|
|
||||||
'footer',
|
|
||||||
'form',
|
|
||||||
'h1',
|
|
||||||
'h2',
|
|
||||||
'h3',
|
|
||||||
'h4',
|
|
||||||
'h5',
|
|
||||||
'h6',
|
|
||||||
'head',
|
|
||||||
'header',
|
|
||||||
'hgroup',
|
|
||||||
'hr',
|
|
||||||
'html',
|
|
||||||
'i',
|
|
||||||
'iframe',
|
|
||||||
'img',
|
|
||||||
'input',
|
|
||||||
'ins',
|
|
||||||
'kbd',
|
|
||||||
'label',
|
|
||||||
'legend',
|
|
||||||
'li',
|
|
||||||
'link',
|
|
||||||
'main',
|
|
||||||
'map',
|
|
||||||
'mark',
|
|
||||||
'menu',
|
|
||||||
'meta',
|
|
||||||
'meter',
|
|
||||||
'nav',
|
|
||||||
'noscript',
|
|
||||||
'object',
|
|
||||||
'ol',
|
|
||||||
'optgroup',
|
|
||||||
'option',
|
|
||||||
'output',
|
|
||||||
'p',
|
|
||||||
'picture',
|
|
||||||
'pre',
|
|
||||||
'progress',
|
|
||||||
'q',
|
|
||||||
'rp',
|
|
||||||
'rt',
|
|
||||||
'ruby',
|
|
||||||
's',
|
|
||||||
'samp',
|
|
||||||
'script',
|
|
||||||
'section',
|
|
||||||
'select',
|
|
||||||
'slot',
|
|
||||||
'small',
|
|
||||||
'source',
|
|
||||||
'span',
|
|
||||||
'strong',
|
|
||||||
'style',
|
|
||||||
'sub',
|
|
||||||
'summary',
|
|
||||||
'sup',
|
|
||||||
'table',
|
|
||||||
'tbody',
|
|
||||||
'td',
|
|
||||||
'template',
|
|
||||||
'textarea',
|
|
||||||
'tfoot',
|
|
||||||
'th',
|
|
||||||
'thead',
|
|
||||||
'time',
|
|
||||||
'title',
|
|
||||||
'tr',
|
|
||||||
'track',
|
|
||||||
'u',
|
|
||||||
'ul',
|
|
||||||
'var',
|
|
||||||
'video',
|
|
||||||
'wbr',
|
|
||||||
'acronym',
|
|
||||||
'applet',
|
|
||||||
'basefont',
|
|
||||||
'bgsound',
|
|
||||||
'big',
|
|
||||||
'blink',
|
|
||||||
'center',
|
|
||||||
'dir',
|
|
||||||
'font',
|
|
||||||
'frame',
|
|
||||||
'frameset',
|
|
||||||
'isindex',
|
|
||||||
'keygen',
|
|
||||||
'listing',
|
|
||||||
'marquee',
|
|
||||||
'menuitem',
|
|
||||||
'multicol',
|
|
||||||
'nextid',
|
|
||||||
'nobr',
|
|
||||||
'noembed',
|
|
||||||
'noframes',
|
|
||||||
'param',
|
|
||||||
'plaintext',
|
|
||||||
'rb',
|
|
||||||
'rtc',
|
|
||||||
'spacer',
|
|
||||||
'strike',
|
|
||||||
'tt',
|
|
||||||
'xmp',
|
|
||||||
'animate',
|
|
||||||
'animateMotion',
|
|
||||||
'animateTransform',
|
|
||||||
'circle',
|
|
||||||
'clipPath',
|
|
||||||
'defs',
|
|
||||||
'desc',
|
|
||||||
'ellipse',
|
|
||||||
'feBlend',
|
|
||||||
'feColorMatrix',
|
|
||||||
'feComponentTransfer',
|
|
||||||
'feComposite',
|
|
||||||
'feConvolveMatrix',
|
|
||||||
'feDiffuseLighting',
|
|
||||||
'feDisplacementMap',
|
|
||||||
'feDistantLight',
|
|
||||||
'feDropShadow',
|
|
||||||
'feFlood',
|
|
||||||
'feFuncA',
|
|
||||||
'feFuncB',
|
|
||||||
'feFuncG',
|
|
||||||
'feFuncR',
|
|
||||||
'feGaussianBlur',
|
|
||||||
'feImage',
|
|
||||||
'feMerge',
|
|
||||||
'feMergeNode',
|
|
||||||
'feMorphology',
|
|
||||||
'feOffset',
|
|
||||||
'fePointLight',
|
|
||||||
'feSpecularLighting',
|
|
||||||
'feSpotLight',
|
|
||||||
'feTile',
|
|
||||||
'feTurbulence',
|
|
||||||
'filter',
|
|
||||||
'foreignObject',
|
|
||||||
'g',
|
|
||||||
'image',
|
|
||||||
'line',
|
|
||||||
'linearGradient',
|
|
||||||
'marker',
|
|
||||||
'mask',
|
|
||||||
'metadata',
|
|
||||||
'mpath',
|
|
||||||
'path',
|
|
||||||
'pattern',
|
|
||||||
'polygon',
|
|
||||||
'polyline',
|
|
||||||
'radialGradient',
|
|
||||||
'rect',
|
|
||||||
'set',
|
|
||||||
'stop',
|
|
||||||
'svg',
|
|
||||||
'switch',
|
|
||||||
'symbol',
|
|
||||||
'text',
|
|
||||||
'textPath',
|
|
||||||
'tspan',
|
|
||||||
'use',
|
|
||||||
'view',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const availableDecoNames = ['Static', 'Prop', 'Env', 'Content', 'Children'];
|
|
||||||
export const dlightDefaultPackageName = '@openinula/next';
|
|
||||||
|
|
||||||
export const importMap = Object.fromEntries(
|
|
||||||
[
|
|
||||||
'createElement',
|
|
||||||
'setStyle',
|
|
||||||
'setDataset',
|
|
||||||
'setEvent',
|
|
||||||
'delegateEvent',
|
|
||||||
'setHTMLProp',
|
|
||||||
'setHTMLAttr',
|
|
||||||
'setHTMLProps',
|
|
||||||
'setHTMLAttrs',
|
|
||||||
'createTextNode',
|
|
||||||
'updateText',
|
|
||||||
'insertNode',
|
|
||||||
'ForNode',
|
|
||||||
'CondNode',
|
|
||||||
'ExpNode',
|
|
||||||
'EnvNode',
|
|
||||||
'TryNode',
|
|
||||||
'SnippetNode',
|
|
||||||
'PropView',
|
|
||||||
'render',
|
|
||||||
].map(name => [name, `$$${name}`])
|
|
||||||
);
|
|
||||||
|
|
||||||
export const importsToDelete = [
|
|
||||||
'Static',
|
|
||||||
'Children',
|
|
||||||
'Content',
|
|
||||||
'Prop',
|
|
||||||
'Env',
|
|
||||||
'Watch',
|
|
||||||
'ForwardProps',
|
|
||||||
'Main',
|
|
||||||
'App',
|
|
||||||
'Mount',
|
|
||||||
'_',
|
|
||||||
'env',
|
|
||||||
'Snippet',
|
|
||||||
...defaultHTMLTags.filter(tag => tag !== 'use'),
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief HTML internal attribute map, can be accessed as js property
|
|
||||||
*/
|
|
||||||
export const defaultAttributeMap = {
|
|
||||||
// ---- Other property as attribute
|
|
||||||
textContent: ['*'],
|
|
||||||
innerHTML: ['*'],
|
|
||||||
// ---- Source: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes
|
|
||||||
accept: ['form', 'input'],
|
|
||||||
// ---- Original: accept-charset
|
|
||||||
acceptCharset: ['form'],
|
|
||||||
accesskey: ['*'],
|
|
||||||
action: ['form'],
|
|
||||||
align: ['caption', 'col', 'colgroup', 'hr', 'iframe', 'img', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr'],
|
|
||||||
allow: ['iframe'],
|
|
||||||
alt: ['area', 'img', 'input'],
|
|
||||||
async: ['script'],
|
|
||||||
autocapitalize: ['*'],
|
|
||||||
autocomplete: ['form', 'input', 'select', 'textarea'],
|
|
||||||
autofocus: ['button', 'input', 'select', 'textarea'],
|
|
||||||
autoplay: ['audio', 'video'],
|
|
||||||
background: ['body', 'table', 'td', 'th'],
|
|
||||||
// ---- Original: base
|
|
||||||
bgColor: ['body', 'col', 'colgroup', 'marquee', 'table', 'tbody', 'tfoot', 'td', 'th', 'tr'],
|
|
||||||
border: ['img', 'object', 'table'],
|
|
||||||
buffered: ['audio', 'video'],
|
|
||||||
capture: ['input'],
|
|
||||||
charset: ['meta'],
|
|
||||||
checked: ['input'],
|
|
||||||
cite: ['blockquote', 'del', 'ins', 'q'],
|
|
||||||
className: ['*'],
|
|
||||||
color: ['font', 'hr'],
|
|
||||||
cols: ['textarea'],
|
|
||||||
// ---- Original: colspan
|
|
||||||
colSpan: ['td', 'th'],
|
|
||||||
content: ['meta'],
|
|
||||||
// ---- Original: contenteditable
|
|
||||||
contentEditable: ['*'],
|
|
||||||
contextmenu: ['*'],
|
|
||||||
controls: ['audio', 'video'],
|
|
||||||
coords: ['area'],
|
|
||||||
crossOrigin: ['audio', 'img', 'link', 'script', 'video'],
|
|
||||||
csp: ['iframe'],
|
|
||||||
data: ['object'],
|
|
||||||
// ---- Original: datetime
|
|
||||||
dateTime: ['del', 'ins', 'time'],
|
|
||||||
decoding: ['img'],
|
|
||||||
default: ['track'],
|
|
||||||
defer: ['script'],
|
|
||||||
dir: ['*'],
|
|
||||||
dirname: ['input', 'textarea'],
|
|
||||||
disabled: ['button', 'fieldset', 'input', 'optgroup', 'option', 'select', 'textarea'],
|
|
||||||
download: ['a', 'area'],
|
|
||||||
draggable: ['*'],
|
|
||||||
enctype: ['form'],
|
|
||||||
// ---- Original: enterkeyhint
|
|
||||||
enterKeyHint: ['textarea', 'contenteditable'],
|
|
||||||
htmlFor: ['label', 'output'],
|
|
||||||
form: ['button', 'fieldset', 'input', 'label', 'meter', 'object', 'output', 'progress', 'select', 'textarea'],
|
|
||||||
// ---- Original: formaction
|
|
||||||
formAction: ['input', 'button'],
|
|
||||||
// ---- Original: formenctype
|
|
||||||
formEnctype: ['button', 'input'],
|
|
||||||
// ---- Original: formmethod
|
|
||||||
formMethod: ['button', 'input'],
|
|
||||||
// ---- Original: formnovalidate
|
|
||||||
formNoValidate: ['button', 'input'],
|
|
||||||
// ---- Original: formtarget
|
|
||||||
formTarget: ['button', 'input'],
|
|
||||||
headers: ['td', 'th'],
|
|
||||||
height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
|
||||||
hidden: ['*'],
|
|
||||||
high: ['meter'],
|
|
||||||
href: ['a', 'area', 'base', 'link'],
|
|
||||||
hreflang: ['a', 'link'],
|
|
||||||
// ---- Original: http-equiv
|
|
||||||
httpEquiv: ['meta'],
|
|
||||||
id: ['*'],
|
|
||||||
integrity: ['link', 'script'],
|
|
||||||
// ---- Original: intrinsicsize
|
|
||||||
intrinsicSize: ['img'],
|
|
||||||
// ---- Original: inputmode
|
|
||||||
inputMode: ['textarea', 'contenteditable'],
|
|
||||||
ismap: ['img'],
|
|
||||||
// ---- Original: itemprop
|
|
||||||
itemProp: ['*'],
|
|
||||||
kind: ['track'],
|
|
||||||
label: ['optgroup', 'option', 'track'],
|
|
||||||
lang: ['*'],
|
|
||||||
language: ['script'],
|
|
||||||
loading: ['img', 'iframe'],
|
|
||||||
list: ['input'],
|
|
||||||
loop: ['audio', 'marquee', 'video'],
|
|
||||||
low: ['meter'],
|
|
||||||
manifest: ['html'],
|
|
||||||
max: ['input', 'meter', 'progress'],
|
|
||||||
// ---- Original: maxlength
|
|
||||||
maxLength: ['input', 'textarea'],
|
|
||||||
// ---- Original: minlength
|
|
||||||
minLength: ['input', 'textarea'],
|
|
||||||
media: ['a', 'area', 'link', 'source', 'style'],
|
|
||||||
method: ['form'],
|
|
||||||
min: ['input', 'meter'],
|
|
||||||
multiple: ['input', 'select'],
|
|
||||||
muted: ['audio', 'video'],
|
|
||||||
name: [
|
|
||||||
'button',
|
|
||||||
'form',
|
|
||||||
'fieldset',
|
|
||||||
'iframe',
|
|
||||||
'input',
|
|
||||||
'object',
|
|
||||||
'output',
|
|
||||||
'select',
|
|
||||||
'textarea',
|
|
||||||
'map',
|
|
||||||
'meta',
|
|
||||||
'param',
|
|
||||||
],
|
|
||||||
// ---- Original: novalidate
|
|
||||||
noValidate: ['form'],
|
|
||||||
open: ['details', 'dialog'],
|
|
||||||
optimum: ['meter'],
|
|
||||||
pattern: ['input'],
|
|
||||||
ping: ['a', 'area'],
|
|
||||||
placeholder: ['input', 'textarea'],
|
|
||||||
// ---- Original: playsinline
|
|
||||||
playsInline: ['video'],
|
|
||||||
poster: ['video'],
|
|
||||||
preload: ['audio', 'video'],
|
|
||||||
readonly: ['input', 'textarea'],
|
|
||||||
// ---- Original: referrerpolicy
|
|
||||||
referrerPolicy: ['a', 'area', 'iframe', 'img', 'link', 'script'],
|
|
||||||
rel: ['a', 'area', 'link'],
|
|
||||||
required: ['input', 'select', 'textarea'],
|
|
||||||
reversed: ['ol'],
|
|
||||||
role: ['*'],
|
|
||||||
rows: ['textarea'],
|
|
||||||
// ---- Original: rowspan
|
|
||||||
rowSpan: ['td', 'th'],
|
|
||||||
sandbox: ['iframe'],
|
|
||||||
scope: ['th'],
|
|
||||||
scoped: ['style'],
|
|
||||||
selected: ['option'],
|
|
||||||
shape: ['a', 'area'],
|
|
||||||
size: ['input', 'select'],
|
|
||||||
sizes: ['link', 'img', 'source'],
|
|
||||||
slot: ['*'],
|
|
||||||
span: ['col', 'colgroup'],
|
|
||||||
spellcheck: ['*'],
|
|
||||||
src: ['audio', 'embed', 'iframe', 'img', 'input', 'script', 'source', 'track', 'video'],
|
|
||||||
srcdoc: ['iframe'],
|
|
||||||
srclang: ['track'],
|
|
||||||
srcset: ['img', 'source'],
|
|
||||||
start: ['ol'],
|
|
||||||
step: ['input'],
|
|
||||||
style: ['*'],
|
|
||||||
summary: ['table'],
|
|
||||||
// ---- Original: tabindex
|
|
||||||
tabIndex: ['*'],
|
|
||||||
target: ['a', 'area', 'base', 'form'],
|
|
||||||
title: ['*'],
|
|
||||||
translate: ['*'],
|
|
||||||
type: ['button', 'input', 'embed', 'object', 'ol', 'script', 'source', 'style', 'menu', 'link'],
|
|
||||||
usemap: ['img', 'input', 'object'],
|
|
||||||
value: ['button', 'data', 'input', 'li', 'meter', 'option', 'progress', 'param', 'text' /** extra for TextNode */],
|
|
||||||
width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
|
||||||
wrap: ['textarea'],
|
|
||||||
// --- ARIA attributes
|
|
||||||
// Source: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
|
|
||||||
ariaAutocomplete: ['*'],
|
|
||||||
ariaChecked: ['*'],
|
|
||||||
ariaDisabled: ['*'],
|
|
||||||
ariaErrorMessage: ['*'],
|
|
||||||
ariaExpanded: ['*'],
|
|
||||||
ariaHasPopup: ['*'],
|
|
||||||
ariaHidden: ['*'],
|
|
||||||
ariaInvalid: ['*'],
|
|
||||||
ariaLabel: ['*'],
|
|
||||||
ariaLevel: ['*'],
|
|
||||||
ariaModal: ['*'],
|
|
||||||
ariaMultiline: ['*'],
|
|
||||||
ariaMultiSelectable: ['*'],
|
|
||||||
ariaOrientation: ['*'],
|
|
||||||
ariaPlaceholder: ['*'],
|
|
||||||
ariaPressed: ['*'],
|
|
||||||
ariaReadonly: ['*'],
|
|
||||||
ariaRequired: ['*'],
|
|
||||||
ariaSelected: ['*'],
|
|
||||||
ariaSort: ['*'],
|
|
||||||
ariaValuemax: ['*'],
|
|
||||||
ariaValuemin: ['*'],
|
|
||||||
ariaValueNow: ['*'],
|
|
||||||
ariaValueText: ['*'],
|
|
||||||
ariaBusy: ['*'],
|
|
||||||
ariaLive: ['*'],
|
|
||||||
ariaRelevant: ['*'],
|
|
||||||
ariaAtomic: ['*'],
|
|
||||||
ariaDropEffect: ['*'],
|
|
||||||
ariaGrabbed: ['*'],
|
|
||||||
ariaActiveDescendant: ['*'],
|
|
||||||
ariaColCount: ['*'],
|
|
||||||
ariaColIndex: ['*'],
|
|
||||||
ariaColSpan: ['*'],
|
|
||||||
ariaControls: ['*'],
|
|
||||||
ariaDescribedBy: ['*'],
|
|
||||||
ariaDescription: ['*'],
|
|
||||||
ariaDetails: ['*'],
|
|
||||||
ariaFlowTo: ['*'],
|
|
||||||
ariaLabelledBy: ['*'],
|
|
||||||
ariaOwns: ['*'],
|
|
||||||
ariaPosInset: ['*'],
|
|
||||||
ariaRowCount: ['*'],
|
|
||||||
ariaRowIndex: ['*'],
|
|
||||||
ariaRowSpan: ['*'],
|
|
||||||
ariaSetSize: ['*'],
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
declare module '@babel/plugin-syntax-do-expressions';
|
|
||||||
declare module '@babel/plugin-syntax-decorators';
|
|
||||||
declare module '@babel/plugin-syntax-jsx';
|
|
||||||
declare module '@babel/plugin-syntax-typescript';
|
|
|
@ -1,37 +0,0 @@
|
||||||
import syntaxDecorators from '@babel/plugin-syntax-decorators';
|
|
||||||
import syntaxJSX from '@babel/plugin-syntax-jsx';
|
|
||||||
import syntaxTypescript from '@babel/plugin-syntax-typescript';
|
|
||||||
import dlight from './plugin';
|
|
||||||
import { type DLightOption } from './types';
|
|
||||||
import { type ConfigAPI, type TransformOptions } from '@babel/core';
|
|
||||||
import { plugin as fn2Class } from '@openinula/class-transformer';
|
|
||||||
import { parse as babelParse } from '@babel/parser';
|
|
||||||
|
|
||||||
export default function (_: ConfigAPI, options: DLightOption): TransformOptions {
|
|
||||||
return {
|
|
||||||
plugins: [
|
|
||||||
syntaxJSX.default ?? syntaxJSX,
|
|
||||||
[syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }],
|
|
||||||
[syntaxDecorators.default ?? syntaxDecorators, { legacy: true }],
|
|
||||||
fn2Class,
|
|
||||||
[dlight, options],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { type DLightOption };
|
|
||||||
|
|
||||||
export function parse(code: string) {
|
|
||||||
const result = babelParse(code, {
|
|
||||||
// parse in strict mode and allow module declarations
|
|
||||||
sourceType: 'module',
|
|
||||||
|
|
||||||
plugins: ['jsx'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.errors.length) {
|
|
||||||
throw new Error(result.errors[0].message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const program = result.program;
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
import type babel from '@babel/core';
|
|
||||||
import { type PluginObj } from '@babel/core';
|
|
||||||
import { PluginProviderClass } from './pluginProvider';
|
|
||||||
import { type DLightOption } from './types';
|
|
||||||
import { defaultAttributeMap } from './const';
|
|
||||||
import { analyze } from './analyzer';
|
|
||||||
|
|
||||||
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,
|
|
||||||
htmlTags = defaultHtmlTags => defaultHtmlTags,
|
|
||||||
attributeMap = defaultAttributeMap,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const pluginProvider = new PluginProviderClass(
|
|
||||||
api,
|
|
||||||
types,
|
|
||||||
Array.isArray(files) ? files : [files],
|
|
||||||
Array.isArray(excludeFiles) ? excludeFiles : [excludeFiles],
|
|
||||||
enableDevTools,
|
|
||||||
htmlTags,
|
|
||||||
attributeMap
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
visitor: {
|
|
||||||
Program: {
|
|
||||||
enter(path, { filename }) {
|
|
||||||
return pluginProvider.programEnterVisitor(path, filename);
|
|
||||||
},
|
|
||||||
exit: pluginProvider.programExitVisitor.bind(pluginProvider),
|
|
||||||
},
|
|
||||||
FunctionDeclaration: {
|
|
||||||
enter: path => {
|
|
||||||
analyze(path);
|
|
||||||
},
|
|
||||||
exit: pluginProvider.classExit.bind(pluginProvider),
|
|
||||||
},
|
|
||||||
ClassMethod: pluginProvider.classMethodVisitor.bind(pluginProvider),
|
|
||||||
ClassProperty: pluginProvider.classPropertyVisitor.bind(pluginProvider),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
import type babel from '@babel/core';
|
|
||||||
import { type PluginObj } from '@babel/core';
|
|
||||||
import { PluginProviderClass } from './pluginProvider';
|
|
||||||
import { type DLightOption } from './types';
|
|
||||||
import { defaultAttributeMap } from './const';
|
|
||||||
|
|
||||||
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,
|
|
||||||
htmlTags = defaultHtmlTags => defaultHtmlTags,
|
|
||||||
attributeMap = defaultAttributeMap,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const pluginProvider = new PluginProviderClass(
|
|
||||||
api,
|
|
||||||
types,
|
|
||||||
Array.isArray(files) ? files : [files],
|
|
||||||
Array.isArray(excludeFiles) ? excludeFiles : [excludeFiles],
|
|
||||||
enableDevTools,
|
|
||||||
htmlTags,
|
|
||||||
attributeMap
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
visitor: {
|
|
||||||
Program: {
|
|
||||||
enter(path, { filename }) {
|
|
||||||
return pluginProvider.programEnterVisitor(path, filename);
|
|
||||||
},
|
|
||||||
exit: pluginProvider.programExitVisitor.bind(pluginProvider),
|
|
||||||
},
|
|
||||||
ClassDeclaration: {
|
|
||||||
enter: pluginProvider.classEnter.bind(pluginProvider),
|
|
||||||
exit: pluginProvider.classExit.bind(pluginProvider),
|
|
||||||
},
|
|
||||||
ClassMethod: pluginProvider.classMethodVisitor.bind(pluginProvider),
|
|
||||||
ClassProperty: pluginProvider.classPropertyVisitor.bind(pluginProvider),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,53 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
|
|
||||||
export type HTMLTags = string[] | ((defaultHtmlTags: string[]) => string[]);
|
|
||||||
export interface DLightOption {
|
|
||||||
/**
|
|
||||||
* Files that will be included
|
|
||||||
* @default ** /*.{js,jsx,ts,tsx}
|
|
||||||
*/
|
|
||||||
files?: string | string[];
|
|
||||||
/**
|
|
||||||
* Files that will be excludes
|
|
||||||
* @default ** /{dist,node_modules,lib}/*.{js,ts}
|
|
||||||
*/
|
|
||||||
excludeFiles?: string | string[];
|
|
||||||
/**
|
|
||||||
* Enable devtools
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
enableDevTools?: boolean;
|
|
||||||
/**
|
|
||||||
* Custom HTML tags.
|
|
||||||
* Accepts 2 types:
|
|
||||||
* 1. string[], e.g. ["div", "span"]
|
|
||||||
* if contains "*", then all default tags will be included
|
|
||||||
* 2. (defaultHtmlTags: string[]) => string[]
|
|
||||||
* @default defaultHtmlTags => defaultHtmlTags
|
|
||||||
*/
|
|
||||||
htmlTags?: HTMLTags;
|
|
||||||
/**
|
|
||||||
* Allowed HTML tags from attributes
|
|
||||||
* e.g. { alt: ["area", "img", "input"] }
|
|
||||||
*/
|
|
||||||
attributeMap?: Record<string, string[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PropertyContainer = Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
node: t.ClassProperty | t.ClassMethod;
|
|
||||||
deps: string[];
|
|
||||||
isStatic?: boolean;
|
|
||||||
isContent?: boolean;
|
|
||||||
isChildren?: boolean | number;
|
|
||||||
isModel?: boolean;
|
|
||||||
isWatcher?: boolean;
|
|
||||||
isPropOrEnv?: 'Prop' | 'Env';
|
|
||||||
depsNode?: t.ArrayExpression;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type IdentifierToDepNode = t.SpreadElement | t.Expression;
|
|
||||||
|
|
||||||
export type SnippetPropSubDepMap = Record<string, Record<string, string[]>>;
|
|
|
@ -1,71 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { transform } from './presets';
|
|
||||||
|
|
||||||
describe('condition', () => {
|
|
||||||
it('should transform jsx', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function App() {
|
|
||||||
return <div>
|
|
||||||
<if cond={count > 1}>{count} is bigger than is 1</if>
|
|
||||||
<else>{count} is smaller than 1</else>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@openinula/next";
|
|
||||||
class App extends View {
|
|
||||||
Body() {
|
|
||||||
let $node0, $node1;
|
|
||||||
this._$update = $changed => {
|
|
||||||
$node1 && $node1.update($changed);
|
|
||||||
};
|
|
||||||
$node0 = $$createElement("div");
|
|
||||||
$node1 = new $$CondNode(0, $thisCond => {
|
|
||||||
if (count > 1) {
|
|
||||||
if ($thisCond.cond === 0) {
|
|
||||||
$thisCond.didntChange = true;
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$thisCond.cond = 0;
|
|
||||||
let $node0, $node1;
|
|
||||||
$thisCond.updateFunc = $changed => {};
|
|
||||||
$node0 = new $$ExpNode(count, []);
|
|
||||||
$node1 = $$createTextNode(" is bigger than is 1", []);
|
|
||||||
return $thisCond.cond === 0 ? [$node0, $node1] : $thisCond.updateCond();
|
|
||||||
} else {
|
|
||||||
if ($thisCond.cond === 1) {
|
|
||||||
$thisCond.didntChange = true;
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$thisCond.cond = 1;
|
|
||||||
let $node0, $node1;
|
|
||||||
$thisCond.updateFunc = $changed => {};
|
|
||||||
$node0 = new $$ExpNode(count, []);
|
|
||||||
$node1 = $$createTextNode(" is smaller than 1", []);
|
|
||||||
return $thisCond.cond === 1 ? [$node0, $node1] : $thisCond.updateCond();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$$insertNode($node0, $node1, 0);
|
|
||||||
$node0._$nodes = [$node1];
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,87 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { isCondNode } from '../src/analyzer';
|
|
||||||
import { mockAnalyze } from './mock';
|
|
||||||
|
|
||||||
describe('analyze early return', () => {
|
|
||||||
it('should work', () => {
|
|
||||||
const root = mockAnalyze(`
|
|
||||||
function App() {
|
|
||||||
if (count > 1) {
|
|
||||||
return <div>1</div>
|
|
||||||
}
|
|
||||||
return <div>
|
|
||||||
<if cond={count > 1}>{count} is bigger than is 1</if>
|
|
||||||
<else>{count} is smaller than 1</else>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
const branchNode = root?.child;
|
|
||||||
if (!isCondNode(branchNode)) {
|
|
||||||
throw new Error('Should be branch node');
|
|
||||||
}
|
|
||||||
expect(branchNode.branches.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with multi if', () => {
|
|
||||||
const root = mockAnalyze(`
|
|
||||||
function App() {
|
|
||||||
if (count > 1) {
|
|
||||||
return <div>1</div>
|
|
||||||
}
|
|
||||||
if (count > 2) {
|
|
||||||
return <div>2</div>
|
|
||||||
}
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
const branchNode = root?.child;
|
|
||||||
if (!isCondNode(branchNode)) {
|
|
||||||
throw new Error('Should be branch node');
|
|
||||||
}
|
|
||||||
expect(branchNode.branches.length).toBe(1);
|
|
||||||
const subBranch = branchNode.child.child;
|
|
||||||
if (!isCondNode(subBranch)) {
|
|
||||||
throw new Error('SubBranchNode should be branch node');
|
|
||||||
}
|
|
||||||
expect(subBranch.branches.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with nested if', () => {
|
|
||||||
const root = mockAnalyze(`
|
|
||||||
function App() {
|
|
||||||
if (count > 1) {
|
|
||||||
if (count > 2) {
|
|
||||||
return <div>2</div>
|
|
||||||
}
|
|
||||||
return <div>1</div>
|
|
||||||
}
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
const branchNode = root?.child;
|
|
||||||
if (!isCondNode(branchNode)) {
|
|
||||||
throw new Error('Should be branch node');
|
|
||||||
}
|
|
||||||
expect(branchNode.branches.length).toBe(1);
|
|
||||||
const subBranchNode = branchNode.branches[0].content.child;
|
|
||||||
if (!isCondNode(subBranchNode)) {
|
|
||||||
throw new Error('SubBranchNode should be branch node');
|
|
||||||
}
|
|
||||||
expect(subBranchNode.branches.length).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,233 +0,0 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { transform } from './presets';
|
|
||||||
|
|
||||||
describe('fn2Class', () => {
|
|
||||||
it('should transform jsx', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
@View
|
|
||||||
class A {
|
|
||||||
Body() {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class A extends View {
|
|
||||||
Body() {
|
|
||||||
let $node0;
|
|
||||||
$node0 = $$createElement("div");
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform jsx with reactive', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
@Main
|
|
||||||
@View
|
|
||||||
class A {
|
|
||||||
count = 1
|
|
||||||
Body() {
|
|
||||||
return <div onClick={() => this.count++}>{this.count}</div>
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class A extends View {
|
|
||||||
count = 1;
|
|
||||||
$$count = 1;
|
|
||||||
Body() {
|
|
||||||
let $node0, $node1;
|
|
||||||
this._$update = $changed => {
|
|
||||||
if ($changed & 1) {
|
|
||||||
$node1 && $node1.update(() => this.count, [this.count]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$node0 = $$createElement("div");
|
|
||||||
$$delegateEvent($node0, "click", () => this._$ud(this.count++, "count"));
|
|
||||||
$node1 = new $$ExpNode(this.count, [this.count]);
|
|
||||||
$$insertNode($node0, $node1, 0);
|
|
||||||
$node0._$nodes = [$node1];
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$$render("main", A);"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform fragment', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
@View
|
|
||||||
class A {
|
|
||||||
Body() {
|
|
||||||
return <>
|
|
||||||
<div></div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class A extends View {
|
|
||||||
Body() {
|
|
||||||
let $node0;
|
|
||||||
$node0 = $$createElement("div");
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform function component', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function MyApp() {
|
|
||||||
let count = 0;
|
|
||||||
return <div onClick={() => count++}>{count}</div>
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class MyApp extends View {
|
|
||||||
count = 0;
|
|
||||||
$$count = 1;
|
|
||||||
Body() {
|
|
||||||
let $node0, $node1;
|
|
||||||
this._$update = $changed => {
|
|
||||||
if ($changed & 1) {
|
|
||||||
$node1 && $node1.update(() => this.count, [this.count]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$node0 = $$createElement("div");
|
|
||||||
$$delegateEvent($node0, "click", () => this._$ud(this.count++, "count"));
|
|
||||||
$node1 = new $$ExpNode(this.count, [this.count]);
|
|
||||||
$$insertNode($node0, $node1, 0);
|
|
||||||
$node0._$nodes = [$node1];
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform function component reactively', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function MyComp() {
|
|
||||||
let count = 0
|
|
||||||
return <>
|
|
||||||
<h1>Hello dlight fn, {count}</h1>
|
|
||||||
<button onClick={() => count +=1}>Add</button>
|
|
||||||
<Button />
|
|
||||||
</>
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class MyComp extends View {
|
|
||||||
count = 0;
|
|
||||||
$$count = 1;
|
|
||||||
Body() {
|
|
||||||
let $node0, $node1, $node2, $node3, $node4;
|
|
||||||
this._$update = $changed => {
|
|
||||||
if ($changed & 1) {
|
|
||||||
$node2 && $node2.update(() => this.count, [this.count]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$node0 = $$createElement("h1");
|
|
||||||
$node1 = $$createTextNode("Hello dlight fn, ", []);
|
|
||||||
$$insertNode($node0, $node1, 0);
|
|
||||||
$node2 = new $$ExpNode(this.count, [this.count]);
|
|
||||||
$$insertNode($node0, $node2, 1);
|
|
||||||
$node0._$nodes = [$node1, $node2];
|
|
||||||
$node3 = $$createElement("button");
|
|
||||||
$$delegateEvent($node3, "click", () => this._$ud(this.count += 1, "count"));
|
|
||||||
$node3.textContent = "Add";
|
|
||||||
$node4 = new Button();
|
|
||||||
$node4._$init(null, null, null, null);
|
|
||||||
return [$node0, $node3, $node4];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform children props', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function App({ children}) {
|
|
||||||
return <h1>{children}</h1>
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class App extends View {
|
|
||||||
get children() {
|
|
||||||
return this._$children;
|
|
||||||
}
|
|
||||||
Body() {
|
|
||||||
let $node0, $node1;
|
|
||||||
$node0 = $$createElement("h1");
|
|
||||||
$node1 = new $$ExpNode(this.children, []);
|
|
||||||
$$insertNode($node0, $node1, 0);
|
|
||||||
$node0._$nodes = [$node1];
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform component composition', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function ArrayModification({name}) {
|
|
||||||
let arr = 1
|
|
||||||
return <section>
|
|
||||||
<div>{arr}</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
|
|
||||||
function MyComp() {
|
|
||||||
return <>
|
|
||||||
<ArrayModification name="1" />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
|
|
||||||
class ArrayModification extends View {
|
|
||||||
$p$name;
|
|
||||||
name;
|
|
||||||
arr = 1;
|
|
||||||
$$arr = 2;
|
|
||||||
Body() {
|
|
||||||
let $node0, $node1, $node2;
|
|
||||||
this._$update = $changed => {
|
|
||||||
if ($changed & 2) {
|
|
||||||
$node2 && $node2.update(() => this.arr, [this.arr]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$node0 = ArrayModification.$t0.cloneNode(true);
|
|
||||||
$node1 = $node0.firstChild;
|
|
||||||
$node2 = new $$ExpNode(this.arr, [this.arr]);
|
|
||||||
$$insertNode($node1, $node2, 0);
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
static $t0 = (() => {
|
|
||||||
let $node0, $node1;
|
|
||||||
$node0 = $$createElement("section");
|
|
||||||
$node1 = $$createElement("div");
|
|
||||||
$node0.appendChild($node1);
|
|
||||||
return $node0;
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
class MyComp extends View {
|
|
||||||
Body() {
|
|
||||||
let $node0;
|
|
||||||
$node0 = new ArrayModification();
|
|
||||||
$node0._$init([["name", "1", []]], null, null, null);
|
|
||||||
return [$node0];
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,79 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ComponentNode, InulaNode } from '../src/analyze/types';
|
|
||||||
import babel, { type PluginObj, transform as transformWithBabel } from '@babel/core';
|
|
||||||
import syntaxJSX from '@babel/plugin-syntax-jsx';
|
|
||||||
import { analyze } from '../src/analyzer';
|
|
||||||
import generate from '@babel/generator';
|
|
||||||
import * as t from '@babel/types';
|
|
||||||
|
|
||||||
export function mockAnalyze(code: string): ComponentNode {
|
|
||||||
let root: ComponentNode | null = null;
|
|
||||||
transformWithBabel(code, {
|
|
||||||
plugins: [
|
|
||||||
syntaxJSX.default ?? syntaxJSX,
|
|
||||||
function (api: typeof babel): PluginObj {
|
|
||||||
const { types } = api;
|
|
||||||
return {
|
|
||||||
visitor: {
|
|
||||||
FunctionDeclaration: {
|
|
||||||
enter: path => {
|
|
||||||
root = analyze(path);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
],
|
|
||||||
filename: 'test.tsx',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!root) {
|
|
||||||
throw new Error('root is null');
|
|
||||||
}
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function genCode(ast: t.Node | null) {
|
|
||||||
if (!ast) {
|
|
||||||
throw new Error('ast is null');
|
|
||||||
}
|
|
||||||
return generate(ast).code;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function printTree(node: InulaNode | undefined): any {
|
|
||||||
if (!node) {
|
|
||||||
return 'empty';
|
|
||||||
}
|
|
||||||
if (node.type === 'cond') {
|
|
||||||
return {
|
|
||||||
type: node.type,
|
|
||||||
branch: node.branches.map(b => printTree(b.content)),
|
|
||||||
children: printTree(node.child),
|
|
||||||
};
|
|
||||||
} else if (node.type === 'comp') {
|
|
||||||
return {
|
|
||||||
type: node.type,
|
|
||||||
children: printTree(node.child),
|
|
||||||
};
|
|
||||||
} else if (node.type === 'jsx') {
|
|
||||||
return {
|
|
||||||
type: node.type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import plugin from '../dist';
|
|
||||||
import { transform as transformWithBabel } from '@babel/core';
|
|
||||||
|
|
||||||
export function transform(code: string) {
|
|
||||||
return transformWithBabel(code, {
|
|
||||||
presets: [plugin],
|
|
||||||
filename: 'test.tsx',
|
|
||||||
})?.code;
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { genCode, mockAnalyze } from './mock';
|
|
||||||
import generate from '@babel/generator';
|
|
||||||
|
|
||||||
describe('propertiesAnalyze', () => {
|
|
||||||
describe('state', () => {
|
|
||||||
it('should work with jsx slice', () => {
|
|
||||||
const root = mockAnalyze(`
|
|
||||||
function App() {
|
|
||||||
const a = <div></div>
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
expect(root.state[0].name).toBe('a');
|
|
||||||
expect(genCode(root.state[0].value)).toMatchInlineSnapshot(`"<$$div-Sub0 />"`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with jsx slice in ternary operator', () => {
|
|
||||||
const root = mockAnalyze(`
|
|
||||||
function App() {
|
|
||||||
const a = true ? <div></div> : <h1></h1>
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
expect(root.state[0].name).toBe('a');
|
|
||||||
expect(root.subComponents[0].name).toBe('$$div-Sub0');
|
|
||||||
expect(genCode(root.subComponents[0].child)).toMatchInlineSnapshot(`"<div></div>"`);
|
|
||||||
expect(root.subComponents[1].name).toBe('$$h1-Sub1');
|
|
||||||
expect(genCode(root.subComponents[1].child)).toMatchInlineSnapshot(`"<h1></h1>"`);
|
|
||||||
expect(genCode(root.state[0].value)).toMatchInlineSnapshot(`"true ? <$$div-Sub0 /> : <$$h1-Sub1 />"`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with jsx slice in arr', () => {
|
|
||||||
const root = mockAnalyze(`
|
|
||||||
function App() {
|
|
||||||
const arr = [<div></div>,<h1></h1>]
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
expect(root.state[0].name).toBe('a');
|
|
||||||
expect(root.subComponents[0].name).toBe('$$div-Sub0');
|
|
||||||
expect(genCode(root.subComponents[0].child)).toMatchInlineSnapshot(`"<div></div>"`);
|
|
||||||
expect(root.subComponents[1].name).toBe('$$h1-Sub1');
|
|
||||||
expect(genCode(root.subComponents[1].child)).toMatchInlineSnapshot(`"<h1></h1>"`);
|
|
||||||
expect(genCode(root.state[0].value)).toMatchInlineSnapshot(`"true ? <$$div-Sub0 /> : <$$h1-Sub1 />"`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ESNext", "DOM"],
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true
|
|
||||||
},
|
|
||||||
"ts-node": {
|
|
||||||
"esm": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
# @openinula/class-transformer
|
|
||||||
|
|
||||||
## 0.0.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- feat: add lifecycles and watch
|
|
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@openinula/class-transformer",
|
|
||||||
"version": "0.0.2",
|
|
||||||
"description": "Inula view generator",
|
|
||||||
"keywords": [
|
|
||||||
"inula"
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.cjs",
|
|
||||||
"module": "dist/index.js",
|
|
||||||
"typings": "dist/index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsup --sourcemap",
|
|
||||||
"test": "vitest --ui"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.20.12",
|
|
||||||
"@babel/generator": "^7.23.6",
|
|
||||||
"@babel/traverse": "^7.24.1",
|
|
||||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
|
||||||
"@types/babel__core": "^7.20.5",
|
|
||||||
"@types/babel__generator": "^7.6.8",
|
|
||||||
"@types/babel__traverse": "^7.6.8",
|
|
||||||
"@vitest/ui": "^0.34.5",
|
|
||||||
"tsup": "^6.7.0",
|
|
||||||
"typescript": "^5.3.2",
|
|
||||||
"vitest": "^0.34.5"
|
|
||||||
},
|
|
||||||
"tsup": {
|
|
||||||
"entry": [
|
|
||||||
"src/index.ts"
|
|
||||||
],
|
|
||||||
"format": [
|
|
||||||
"cjs",
|
|
||||||
"esm"
|
|
||||||
],
|
|
||||||
"clean": true,
|
|
||||||
"dts": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Option } from './types';
|
|
||||||
import type { ConfigAPI, TransformOptions } from '@babel/core';
|
|
||||||
import transformer from './plugin';
|
|
||||||
|
|
||||||
export default function (_: ConfigAPI, options: Option): TransformOptions {
|
|
||||||
return {
|
|
||||||
plugins: [
|
|
||||||
['@babel/plugin-syntax-jsx'],
|
|
||||||
['@babel/plugin-syntax-typescript', { isTSX: true }],
|
|
||||||
[transformer, options],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export const plugin = transformer;
|
|
||||||
|
|
||||||
export type { Option };
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NodePath, PluginObj } from '@babel/core';
|
|
||||||
import { Option } from './types';
|
|
||||||
import * as babel from '@babel/core';
|
|
||||||
import { PluginProvider } from './pluginProvider';
|
|
||||||
import { ThisPatcher } from './thisPatcher';
|
|
||||||
|
|
||||||
export default function (api: typeof babel, options: Option): PluginObj {
|
|
||||||
const pluginProvider = new PluginProvider(api, options);
|
|
||||||
const thisPatcher = new ThisPatcher(api);
|
|
||||||
return {
|
|
||||||
name: 'zouyu-2',
|
|
||||||
visitor: {
|
|
||||||
FunctionDeclaration(path) {
|
|
||||||
pluginProvider.functionDeclarationVisitor(path);
|
|
||||||
|
|
||||||
thisPatcher.patch(path as unknown as NodePath<babel.types.Class>);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,423 +0,0 @@
|
||||||
import { type types as t, NodePath } from '@babel/core';
|
|
||||||
import * as babel from '@babel/core';
|
|
||||||
import { Option } from './types';
|
|
||||||
import type { Scope } from '@babel/traverse';
|
|
||||||
|
|
||||||
const DECORATOR_PROPS = 'Prop';
|
|
||||||
const DECORATOR_CHILDREN = 'Children';
|
|
||||||
const DECORATOR_WATCH = 'Watch';
|
|
||||||
const DECORATOR_ENV = 'Env';
|
|
||||||
|
|
||||||
function replaceFnWithClass(path: NodePath<t.FunctionDeclaration>, classTransformer: ClassComponentTransformer) {
|
|
||||||
const originalName = path.node.id.name;
|
|
||||||
const tempName = path.node.id.name + 'Temp';
|
|
||||||
const classComp = classTransformer.genClassComponent(tempName);
|
|
||||||
path.replaceWith(classComp);
|
|
||||||
path.scope.rename(tempName, originalName);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PluginProvider {
|
|
||||||
// ---- Plugin Level ----
|
|
||||||
private readonly babelApi: typeof babel;
|
|
||||||
private readonly t: typeof t;
|
|
||||||
|
|
||||||
private programNode: t.Program | undefined;
|
|
||||||
|
|
||||||
constructor(babelApi: typeof babel, options: Option) {
|
|
||||||
this.babelApi = babelApi;
|
|
||||||
this.t = babelApi.types;
|
|
||||||
}
|
|
||||||
|
|
||||||
functionDeclarationVisitor(path: NodePath<t.FunctionDeclaration>): void {
|
|
||||||
// find Component function by:
|
|
||||||
// 1. has JSXElement as return value
|
|
||||||
// 2. name is capitalized
|
|
||||||
if (path.node.id?.name[0] !== path.node.id?.name[0].toUpperCase()) return;
|
|
||||||
const returnStatement = path.node.body.body.find(n => this.t.isReturnStatement(n)) as t.ReturnStatement;
|
|
||||||
if (!returnStatement) return;
|
|
||||||
if (!(this.t.isJSXElement(returnStatement.argument) || this.t.isJSXFragment(returnStatement.argument))) return;
|
|
||||||
const classTransformer = new ClassComponentTransformer(this.babelApi, path);
|
|
||||||
// transform the parameters to props
|
|
||||||
const params = path.node.params;
|
|
||||||
// ---
|
|
||||||
const props = params[0];
|
|
||||||
classTransformer.transformProps(props);
|
|
||||||
|
|
||||||
// --- env
|
|
||||||
const env = params[1];
|
|
||||||
classTransformer.transformEnv(env);
|
|
||||||
|
|
||||||
// iterate the function body orderly
|
|
||||||
const body = path.node.body.body;
|
|
||||||
body.forEach((node, idx) => {
|
|
||||||
if (this.t.isVariableDeclaration(node)) {
|
|
||||||
classTransformer.transformStateDeclaration(node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// handle method
|
|
||||||
if (this.t.isFunctionDeclaration(node)) {
|
|
||||||
classTransformer.transformMethods(node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- handle lifecycles
|
|
||||||
const lifecycles = ['willMount', 'didMount', 'willUnmount', 'didUnmount'];
|
|
||||||
if (this.t.isLabeledStatement(node) && lifecycles.includes(node.label.name)) {
|
|
||||||
// transform the lifecycle statement to lifecycle method
|
|
||||||
classTransformer.transformLifeCycle(node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle watch
|
|
||||||
if (this.t.isLabeledStatement(node) && node.label.name === 'watch') {
|
|
||||||
// transform the watch statement to watch method
|
|
||||||
classTransformer.transformWatch(node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle return statement
|
|
||||||
if (this.t.isReturnStatement(node)) {
|
|
||||||
// handle early return
|
|
||||||
if (idx !== body.length - 1) {
|
|
||||||
// transform the return statement to render method
|
|
||||||
// TODO: handle early return
|
|
||||||
throw new Error('Early return is not supported yet.');
|
|
||||||
}
|
|
||||||
// transform the return statement to render method
|
|
||||||
classTransformer.transformRenderMethod(node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// replace the function declaration with class declaration
|
|
||||||
replaceFnWithClass(path, classTransformer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToWatchNode =
|
|
||||||
| t.ExpressionStatement
|
|
||||||
| t.ForStatement
|
|
||||||
| t.WhileStatement
|
|
||||||
| t.IfStatement
|
|
||||||
| t.SwitchStatement
|
|
||||||
| t.TryStatement;
|
|
||||||
|
|
||||||
class ClassComponentTransformer {
|
|
||||||
properties: (t.ClassProperty | t.ClassMethod)[] = [];
|
|
||||||
// The expression to bind the nested destructuring props with prop
|
|
||||||
nestedDestructuringBindings: t.Expression[] = [];
|
|
||||||
private readonly babelApi: typeof babel;
|
|
||||||
private readonly t: typeof t;
|
|
||||||
private readonly functionScope: Scope;
|
|
||||||
|
|
||||||
valueWrapper(node) {
|
|
||||||
return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
addProperty(prop: t.ClassProperty | t.ClassMethod, name?: string) {
|
|
||||||
this.properties.push(prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(babelApi: typeof babel, fnNode: NodePath<t.FunctionDeclaration>) {
|
|
||||||
this.babelApi = babelApi;
|
|
||||||
this.t = babelApi.types;
|
|
||||||
// get the function body scope
|
|
||||||
this.functionScope = fnNode.scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
// transform function component to class component extends View
|
|
||||||
genClassComponent(name: string) {
|
|
||||||
// generate ctor and push this.initExpressions to ctor
|
|
||||||
let nestedDestructuringBindingsMethod: t.ClassMethod | null = null;
|
|
||||||
if (this.nestedDestructuringBindings.length) {
|
|
||||||
nestedDestructuringBindingsMethod = this.t.classMethod(
|
|
||||||
'method',
|
|
||||||
this.t.identifier('$$bindNestDestructuring'),
|
|
||||||
[],
|
|
||||||
this.t.blockStatement([...this.nestedDestructuringBindings.map(exp => this.t.expressionStatement(exp))])
|
|
||||||
);
|
|
||||||
nestedDestructuringBindingsMethod.decorators = [this.t.decorator(this.t.identifier(DECORATOR_WATCH))];
|
|
||||||
}
|
|
||||||
return this.t.classDeclaration(
|
|
||||||
this.t.identifier(name),
|
|
||||||
this.t.identifier('View'),
|
|
||||||
this.t.classBody(
|
|
||||||
nestedDestructuringBindingsMethod ? [...this.properties, nestedDestructuringBindingsMethod] : this.properties
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform state declaration to class property
|
|
||||||
* if the variable is declared with `let` or `const`, it should be transformed to class property
|
|
||||||
* @param node
|
|
||||||
*/
|
|
||||||
transformStateDeclaration(node: t.VariableDeclaration) {
|
|
||||||
// iterate the declarations
|
|
||||||
node.declarations.forEach(declaration => {
|
|
||||||
const id = declaration.id;
|
|
||||||
// handle destructuring
|
|
||||||
if (this.t.isObjectPattern(id)) {
|
|
||||||
return this.transformPropsDestructuring(id);
|
|
||||||
} else if (this.t.isArrayPattern(id)) {
|
|
||||||
// TODO: handle array destructuring
|
|
||||||
} else if (this.t.isIdentifier(id)) {
|
|
||||||
// clone the id
|
|
||||||
const cloneId = this.t.cloneNode(id);
|
|
||||||
this.addProperty(this.t.classProperty(cloneId, declaration.init), id.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform render method to Body method
|
|
||||||
* The Body method should return the original return statement
|
|
||||||
* @param node
|
|
||||||
*/
|
|
||||||
transformRenderMethod(node: t.ReturnStatement) {
|
|
||||||
const body = this.t.classMethod(
|
|
||||||
'method',
|
|
||||||
this.t.identifier('Body'),
|
|
||||||
[],
|
|
||||||
this.t.blockStatement([node]),
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
this.addProperty(body, 'Body');
|
|
||||||
}
|
|
||||||
|
|
||||||
transformLifeCycle(node: t.LabeledStatement) {
|
|
||||||
// transform the lifecycle statement to lifecycle method
|
|
||||||
const methodName = node.label.name;
|
|
||||||
const method = this.t.classMethod(
|
|
||||||
'method',
|
|
||||||
this.t.identifier(methodName),
|
|
||||||
[],
|
|
||||||
this.t.blockStatement(node.body.body),
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
this.addProperty(method, methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
transformComputed() {}
|
|
||||||
|
|
||||||
transformMethods(node: t.FunctionDeclaration) {
|
|
||||||
// transform the function declaration to class method
|
|
||||||
const methodName = node.id?.name;
|
|
||||||
if (!methodName) return;
|
|
||||||
const method = this.t.classMethod(
|
|
||||||
'method',
|
|
||||||
this.t.identifier(methodName),
|
|
||||||
node.params,
|
|
||||||
node.body,
|
|
||||||
node.generator,
|
|
||||||
node.async
|
|
||||||
);
|
|
||||||
this.addProperty(method, methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
transformProps(param: t.Identifier | t.RestElement | t.Pattern) {
|
|
||||||
if (!param) return;
|
|
||||||
// handle destructuring
|
|
||||||
if (this.isObjDestructuring(param)) {
|
|
||||||
this.transformPropsDestructuring(param);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.t.isIdentifier(param)) {
|
|
||||||
// TODO: handle props identifier
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error('Unsupported props type, please use object destructuring or identifier.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* transform node to watch label to watch decorator
|
|
||||||
* e.g.
|
|
||||||
*
|
|
||||||
* watch: console.log(state)
|
|
||||||
* // transform into
|
|
||||||
* @Watch
|
|
||||||
* _watch() {
|
|
||||||
* console.log(state)
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
transformWatch(node: t.LabeledStatement) {
|
|
||||||
const id = this.functionScope.generateUidIdentifier(DECORATOR_WATCH.toLowerCase());
|
|
||||||
const method = this.t.classMethod('method', id, [], this.t.blockStatement([node.body]), false, false);
|
|
||||||
method.decorators = [this.t.decorator(this.t.identifier(DECORATOR_WATCH))];
|
|
||||||
this.addProperty(method);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isObjDestructuring(param: t.Identifier | t.RestElement | t.Pattern): param is t.ObjectPattern {
|
|
||||||
return this.t.isObjectPattern(param);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* how to handle default value
|
|
||||||
* ```js
|
|
||||||
* // 1. No alias
|
|
||||||
* function({name = 'defaultName'}) {}
|
|
||||||
* class A extends View {
|
|
||||||
* @Prop name = 'defaultName';
|
|
||||||
*
|
|
||||||
* // 2. Alias
|
|
||||||
* function({name: aliasName = 'defaultName'}) {}
|
|
||||||
* class A extends View {
|
|
||||||
* @Prop name = 'defaultName';
|
|
||||||
* aliasName
|
|
||||||
* @Watch
|
|
||||||
* bindAliasName() {
|
|
||||||
* this.aliasName = this.name;
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // 3. Children with default value and alias
|
|
||||||
* function({children: aliasName = 'defaultName'}) {}
|
|
||||||
* class A extends View {
|
|
||||||
* @Children aliasName = 'defaultName';
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
private transformPropsDestructuring(param: t.ObjectPattern) {
|
|
||||||
const propNames: t.Identifier[] = [];
|
|
||||||
param.properties.forEach(prop => {
|
|
||||||
if (this.t.isObjectProperty(prop)) {
|
|
||||||
let key = prop.key;
|
|
||||||
let defaultVal: t.Expression;
|
|
||||||
if (this.t.isIdentifier(key)) {
|
|
||||||
let alias: t.Identifier | null = null;
|
|
||||||
if (this.t.isAssignmentPattern(prop.value)) {
|
|
||||||
const propName = prop.value.left;
|
|
||||||
defaultVal = prop.value.right;
|
|
||||||
if (this.t.isIdentifier(propName)) {
|
|
||||||
// handle alias
|
|
||||||
if (propName.name !== key.name) {
|
|
||||||
alias = propName;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw Error(`Unsupported assignment type in object destructuring: ${propName.type}`);
|
|
||||||
}
|
|
||||||
} else if (this.t.isIdentifier(prop.value)) {
|
|
||||||
// handle alias
|
|
||||||
if (key.name !== prop.value.name) {
|
|
||||||
alias = prop.value;
|
|
||||||
}
|
|
||||||
} else if (this.t.isObjectPattern(prop.value)) {
|
|
||||||
// TODO: handle nested destructuring
|
|
||||||
this.transformPropsDestructuring(prop.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isChildren = key.name === 'children';
|
|
||||||
if (alias) {
|
|
||||||
if (isChildren) {
|
|
||||||
key = alias;
|
|
||||||
} else {
|
|
||||||
this.addClassPropertyForPropAlias(alias, key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.addClassProperty(key, isChildren ? DECORATOR_CHILDREN : DECORATOR_PROPS, defaultVal);
|
|
||||||
propNames.push(key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle default value
|
|
||||||
if (this.t.isAssignmentPattern(prop.value)) {
|
|
||||||
const defaultValue = prop.value.right;
|
|
||||||
const propName = prop.value.left;
|
|
||||||
//handle alias
|
|
||||||
if (this.t.isIdentifier(propName) && propName.name !== prop.key.name) {
|
|
||||||
this.addClassProperty(propName, null, undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.t.isIdentifier(propName)) {
|
|
||||||
this.addClassProperty(propName, DECORATOR_PROPS, defaultValue);
|
|
||||||
propNames.push(propName);
|
|
||||||
}
|
|
||||||
// TODO: handle nested destructuring
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error('Unsupported props destructuring, please use simple object destructuring.');
|
|
||||||
} else {
|
|
||||||
// TODO: handle rest element
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return propNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
private addClassPropertyForPropAlias(propName: t.Identifier, key: t.Identifier) {
|
|
||||||
// handle alias, like class A { foo: bar = 'default' }
|
|
||||||
this.addClassProperty(propName, null, undefined);
|
|
||||||
// push alias assignment in Watch , like this.bar = this.foo
|
|
||||||
this.nestedDestructuringBindings.push(
|
|
||||||
this.t.assignmentExpression('=', this.t.identifier(propName.name), this.t.identifier(key.name))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add prop to class, like @prop name = '';
|
|
||||||
private addClassProperty(key: t.Identifier, decorator: string | null, defaultValue?: t.Expression) {
|
|
||||||
// clone the key to avoid reference issue
|
|
||||||
const id = this.t.cloneNode(key);
|
|
||||||
this.addProperty(
|
|
||||||
this.t.classProperty(
|
|
||||||
id,
|
|
||||||
defaultValue ?? undefined,
|
|
||||||
undefined,
|
|
||||||
// use prop decorator
|
|
||||||
decorator ? [this.t.decorator(this.t.identifier(decorator))] : undefined,
|
|
||||||
undefined,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
key.name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: need refactor, maybe merge with props?
|
|
||||||
transformEnv(env: t.Identifier | t.Pattern | t.RestElement) {
|
|
||||||
if (!env) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.t.isObjectPattern(env)) {
|
|
||||||
throw Error('Unsupported env type, please use object destructuring.');
|
|
||||||
}
|
|
||||||
env.properties.forEach(property => {
|
|
||||||
if (this.t.isObjectProperty(property)) {
|
|
||||||
const key = property.key;
|
|
||||||
let defaultVal: t.Expression;
|
|
||||||
if (this.t.isIdentifier(key)) {
|
|
||||||
let alias: t.Identifier | null = null;
|
|
||||||
if (this.t.isAssignmentPattern(property.value)) {
|
|
||||||
const propName = property.value.left;
|
|
||||||
defaultVal = property.value.right;
|
|
||||||
if (this.t.isIdentifier(propName)) {
|
|
||||||
// handle alias
|
|
||||||
if (propName.name !== key.name) {
|
|
||||||
alias = propName;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw Error(`Unsupported assignment type in object destructuring: ${propName.type}`);
|
|
||||||
}
|
|
||||||
} else if (this.t.isIdentifier(property.value)) {
|
|
||||||
// handle alias
|
|
||||||
if (key.name !== property.value.name) {
|
|
||||||
alias = property.value;
|
|
||||||
}
|
|
||||||
} else if (this.t.isObjectPattern(property.value)) {
|
|
||||||
throw Error('Unsupported nested env destructuring');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alias) {
|
|
||||||
this.addClassPropertyForPropAlias(alias, key);
|
|
||||||
}
|
|
||||||
this.addClassProperty(key, DECORATOR_ENV, defaultVal);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error('Unsupported props destructuring, please use simple object destructuring.');
|
|
||||||
} else {
|
|
||||||
throw new Error('Unsupported env destructuring, please use plain object destructuring.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,203 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { transform } from './transform';
|
|
||||||
|
|
||||||
describe('component-composition', () => {
|
|
||||||
describe('props destructuring', () => {
|
|
||||||
it('should support default values', () => {
|
|
||||||
//language=JSX
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function UserProfile({
|
|
||||||
name = '',
|
|
||||||
age = null,
|
|
||||||
favouriteColors = [],
|
|
||||||
isAvailable = false,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p>My name is {name}!</p>
|
|
||||||
<p>My age is {age}!</p>
|
|
||||||
<p>My favourite colors are {favouriteColors.join(', ')}!</p>
|
|
||||||
<p>I am {isAvailable ? 'available' : 'not available'}</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}`),
|
|
||||||
`
|
|
||||||
class UserProfile {
|
|
||||||
@Prop name = ''
|
|
||||||
@Prop age = null
|
|
||||||
@Prop favouriteColors = []
|
|
||||||
@Prop isAvailable = false
|
|
||||||
|
|
||||||
Body() {
|
|
||||||
p(\`My name is \${this.name}!\`)
|
|
||||||
p(\`My age is \${this.age}!\`)
|
|
||||||
p(\`My favourite colors are \${this.favouriteColors.join(', ')}!\`)
|
|
||||||
p(\`I am \${this.isAvailable ? 'available' : 'not available'}\`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support nested destruing', () => {
|
|
||||||
//language=JSX
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function UserProfile({
|
|
||||||
name = '',
|
|
||||||
age = null,
|
|
||||||
favouriteColors: [{r, g, b}, color2],
|
|
||||||
isAvailable = false,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p>My name is {name}!</p>
|
|
||||||
<p>My age is {age}!</p>
|
|
||||||
<p>My favourite colors are {favouriteColors.join(', ')}!</p>
|
|
||||||
<p>I am {isAvailable ? 'available' : 'not available'}</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}`),
|
|
||||||
`
|
|
||||||
class UserProfile {
|
|
||||||
@Prop name = '';
|
|
||||||
@Prop age = null;
|
|
||||||
@Prop favouriteColors = [];
|
|
||||||
@Prop isAvailable = false;
|
|
||||||
color1;
|
|
||||||
color2;
|
|
||||||
r;
|
|
||||||
g;
|
|
||||||
b;
|
|
||||||
xx = (() => {
|
|
||||||
const [{r, g, b}, color2] = this.favouriteColors;
|
|
||||||
this.r = r
|
|
||||||
this.g = g
|
|
||||||
this.b = b
|
|
||||||
this.color2 = color2
|
|
||||||
});
|
|
||||||
|
|
||||||
Body() {
|
|
||||||
p(\`My name is \${this.name}!\`);
|
|
||||||
p(\`My age is \${this.age}!\`);
|
|
||||||
p(\`My favourite colors are \${this.favouriteColors.join(', ')}!\`);
|
|
||||||
p(\`I am \${this.isAvailable ? 'available' : 'not available'}\`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support children prop', () => {
|
|
||||||
//language=JSX
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function Card({children}) {
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}`),
|
|
||||||
`
|
|
||||||
class Card {
|
|
||||||
@Children children
|
|
||||||
|
|
||||||
Body() {
|
|
||||||
div(\`card\`, this.children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('env', () => {
|
|
||||||
it('should support env', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function App () {
|
|
||||||
return <Env theme="dark">
|
|
||||||
<Child name="child"/>
|
|
||||||
</Env>;
|
|
||||||
}
|
|
||||||
function Child({ name },{ theme }){
|
|
||||||
return <div>{theme}</div>
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class App extends View {
|
|
||||||
Body() {
|
|
||||||
return <env theme=\\"dark\\">
|
|
||||||
<Child name=\\"child\\" />
|
|
||||||
</env>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class Child extends View {
|
|
||||||
@Prop
|
|
||||||
name;
|
|
||||||
@Env
|
|
||||||
theme;
|
|
||||||
Body() {
|
|
||||||
return <div>{this.theme}</div>;
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should support children prop with alias', () => {
|
|
||||||
//language=JSX
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function Card({children: content, foo: bar = 1, val = 1}) {
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class Card extends View {
|
|
||||||
@Children
|
|
||||||
content;
|
|
||||||
bar;
|
|
||||||
@Prop
|
|
||||||
foo = 1;
|
|
||||||
@Prop
|
|
||||||
val = 1;
|
|
||||||
Body()
|
|
||||||
{
|
|
||||||
return <div className=\\
|
|
||||||
"card\\">
|
|
||||||
{
|
|
||||||
this.content
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
;
|
|
||||||
}
|
|
||||||
@Watch
|
|
||||||
$$bindNestDestructuring()
|
|
||||||
{
|
|
||||||
this.bar = this.foo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test, it, expect } from 'vitest';
|
|
||||||
import { transform } from './transform';
|
|
||||||
|
|
||||||
test('conditional', () => {});
|
|
|
@ -1,114 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { transform } from './transform';
|
|
||||||
|
|
||||||
describe('lifecycle', () => {
|
|
||||||
it('should support willMount', () => {
|
|
||||||
//language=JSX
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function App() {
|
|
||||||
willMount: {
|
|
||||||
console.log('willMount')
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div/>
|
|
||||||
);
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class App extends View {
|
|
||||||
willMount() {
|
|
||||||
console.log('willMount');
|
|
||||||
}
|
|
||||||
Body() {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
it('should support didMount', () => {
|
|
||||||
//language=JSX
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function App() {
|
|
||||||
didMount: {
|
|
||||||
console.log('didMount');
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div/>
|
|
||||||
);
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class App extends View {
|
|
||||||
didMount() {
|
|
||||||
console.log('didMount');
|
|
||||||
}
|
|
||||||
Body() {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support willUnmount', () => {
|
|
||||||
//language=JSX
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function App() {
|
|
||||||
willUnmount: {
|
|
||||||
console.log('willUnmount');
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div/>
|
|
||||||
);
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class App extends View {
|
|
||||||
willUnmount() {
|
|
||||||
console.log('willUnmount');
|
|
||||||
}
|
|
||||||
Body() {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support didUnmount', () => {
|
|
||||||
//language=JSX
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function App() {
|
|
||||||
didUnmount: {
|
|
||||||
console.log('didUnmount');
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div/>
|
|
||||||
);
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class App extends View {
|
|
||||||
didUnmount() {
|
|
||||||
console.log('didUnmount');
|
|
||||||
}
|
|
||||||
Body() {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,277 +0,0 @@
|
||||||
import { it, describe, expect } from 'vitest';
|
|
||||||
import { transform } from './transform';
|
|
||||||
|
|
||||||
describe('reactivity', () => {
|
|
||||||
describe('state', () => {
|
|
||||||
it('should transform state assignment', () => {
|
|
||||||
expect(
|
|
||||||
//language=JSX
|
|
||||||
transform(`
|
|
||||||
export default function Name() {
|
|
||||||
let name = 'John';
|
|
||||||
|
|
||||||
return <h1>{name}</h1>;
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class Name extends View {
|
|
||||||
name = 'John';
|
|
||||||
Body() {
|
|
||||||
return <h1>{this.name}</h1>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export { Name as default };"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform state modification ', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function MyApp() {
|
|
||||||
let count = 0;
|
|
||||||
return <div onClick={() => count++}>{count}</div>
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class MyApp extends View {
|
|
||||||
count = 0;
|
|
||||||
Body() {
|
|
||||||
return <div onClick={() => this.count++}>{this.count}</div>;
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not transform variable out of scope', () => {
|
|
||||||
expect(
|
|
||||||
//language=JSX
|
|
||||||
transform(`
|
|
||||||
const name = "John";
|
|
||||||
export default function Name() {
|
|
||||||
return <h1>{name}</h1>;
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"const name = \\"John\\";
|
|
||||||
class Name extends View {
|
|
||||||
Body() {
|
|
||||||
return <h1>{name}</h1>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export { Name as default };"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform function declaration', () => {
|
|
||||||
expect(
|
|
||||||
//language=JSX
|
|
||||||
transform(`
|
|
||||||
const name = "John";
|
|
||||||
|
|
||||||
function Name() {
|
|
||||||
function getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
console.log(getName());
|
|
||||||
}
|
|
||||||
return <h1 onClick={onClick}>{name}</h1>;
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"const name = \\"John\\";
|
|
||||||
class Name extends View {
|
|
||||||
getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
onClick = () => {
|
|
||||||
console.log(this.getName());
|
|
||||||
};
|
|
||||||
Body() {
|
|
||||||
return <h1 onClick={this.onClick}>{name}</h1>;
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not transform function parameter to this', () => {
|
|
||||||
expect(
|
|
||||||
//language=JSX
|
|
||||||
transform(`
|
|
||||||
function Name() {
|
|
||||||
let name = 'Doe'
|
|
||||||
|
|
||||||
function getName(name) {
|
|
||||||
return name + '!'
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
console.log(getName('John'));
|
|
||||||
}
|
|
||||||
return <h1 onClick={onClick}>{name}</h1>;
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class Name extends View {
|
|
||||||
name = 'Doe';
|
|
||||||
getName(name) {
|
|
||||||
return name + '!';
|
|
||||||
}
|
|
||||||
onClick = () => {
|
|
||||||
console.log(this.getName('John'));
|
|
||||||
};
|
|
||||||
Body() {
|
|
||||||
return <h1 onClick={this.onClick}>{this.name}</h1>;
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform derived assignment', () => {
|
|
||||||
expect(
|
|
||||||
//language=JSX
|
|
||||||
transform(`
|
|
||||||
export default function NameComp() {
|
|
||||||
let firstName = "John";
|
|
||||||
let lastName = "Doe";
|
|
||||||
let fullName = \`\${firstName} \${lastName}\`
|
|
||||||
|
|
||||||
return <h1>{fullName}</h1>;
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class NameComp extends View {
|
|
||||||
firstName = \\"John\\";
|
|
||||||
lastName = \\"Doe\\";
|
|
||||||
fullName = \`\${this.firstName} \${this.lastName}\`;
|
|
||||||
Body() {
|
|
||||||
return <h1>{this.fullName}</h1>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export { NameComp as default };"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('watch', () => {
|
|
||||||
it('should transform watch from call expression', () => {
|
|
||||||
expect(
|
|
||||||
//language=JSX
|
|
||||||
transform(`
|
|
||||||
export default function CountComp() {
|
|
||||||
let count = 0;
|
|
||||||
watch: console.log(count);
|
|
||||||
|
|
||||||
return <div>{count}</div>;
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class CountComp extends View {
|
|
||||||
count = 0;
|
|
||||||
@Watch
|
|
||||||
_watch() {
|
|
||||||
console.log(this.count);
|
|
||||||
}
|
|
||||||
Body() {
|
|
||||||
return <div>{this.count}</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export { CountComp as default };"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform watch from block statement', () => {
|
|
||||||
expect(
|
|
||||||
//language=JSX
|
|
||||||
transform(`
|
|
||||||
export default function CountComp() {
|
|
||||||
let count = 0;
|
|
||||||
watch: for (let i = 0; i < count; i++) {
|
|
||||||
console.log(\`The count change to: \${i}\`);
|
|
||||||
}
|
|
||||||
return <>
|
|
||||||
<button onClick={() => count++}>Add</button>
|
|
||||||
<div>{count}</div>
|
|
||||||
</>;
|
|
||||||
};
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(
|
|
||||||
`
|
|
||||||
"class CountComp extends View {
|
|
||||||
count = 0;
|
|
||||||
@Watch
|
|
||||||
_watch() {
|
|
||||||
for (let i = 0; i < this.count; i++) {
|
|
||||||
console.log(\`The count change to: \${i}\`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Body() {
|
|
||||||
return <>
|
|
||||||
<button onClick={() => this.count++}>Add</button>
|
|
||||||
<div>{this.count}</div>
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export { CountComp as default };
|
|
||||||
;"
|
|
||||||
`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform watch from if statement', () => {
|
|
||||||
expect(
|
|
||||||
//language=JSX
|
|
||||||
transform(`
|
|
||||||
export default function CountComp() {
|
|
||||||
let count = 0;
|
|
||||||
watch: if (count > 0) {
|
|
||||||
console.log(\`The count is greater than 0\`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>{count}</div>;
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class CountComp extends View {
|
|
||||||
count = 0;
|
|
||||||
@Watch
|
|
||||||
_watch() {
|
|
||||||
if (this.count > 0) {
|
|
||||||
console.log(\`The count is greater than 0\`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Body() {
|
|
||||||
return <div>{this.count}</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export { CountComp as default };"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transform function component reactively', () => {
|
|
||||||
expect(
|
|
||||||
transform(`
|
|
||||||
function MyComp() {
|
|
||||||
let count = 0
|
|
||||||
return <>
|
|
||||||
<h1 count='123'>Hello dlight fn, {count}</h1>
|
|
||||||
<button onClick={() => count +=1}>Add</button>
|
|
||||||
<Button />
|
|
||||||
</>
|
|
||||||
}`)
|
|
||||||
).toMatchInlineSnapshot(`
|
|
||||||
"class MyComp extends View {
|
|
||||||
count = 0;
|
|
||||||
Body() {
|
|
||||||
return <>
|
|
||||||
<h1 count='123'>Hello dlight fn, {this.count}</h1>
|
|
||||||
<button onClick={() => this.count += 1}>Add</button>
|
|
||||||
<Button />
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { transform as transformWithBabel } from '@babel/core';
|
|
||||||
import plugin from '../';
|
|
||||||
|
|
||||||
export function transform(code: string) {
|
|
||||||
return transformWithBabel(code, {
|
|
||||||
presets: [plugin],
|
|
||||||
})?.code;
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
import { type types as t, NodePath } from '@babel/core';
|
|
||||||
import * as babel from '@babel/core';
|
|
||||||
|
|
||||||
export class ThisPatcher {
|
|
||||||
private readonly babelApi: typeof babel;
|
|
||||||
private readonly t: typeof t;
|
|
||||||
|
|
||||||
private programNode: t.Program | undefined;
|
|
||||||
|
|
||||||
constructor(babelApi: typeof babel) {
|
|
||||||
this.babelApi = babelApi;
|
|
||||||
this.t = babelApi.types;
|
|
||||||
}
|
|
||||||
|
|
||||||
patch = (classPath: NodePath<t.Class>) => {
|
|
||||||
const classBodyNode = classPath.node.body;
|
|
||||||
const availPropNames = classBodyNode.body
|
|
||||||
.filter(
|
|
||||||
(def): def is Exclude<t.ClassBody['body'][number], t.TSIndexSignature | t.StaticBlock> =>
|
|
||||||
!this.t.isTSIndexSignature(def) && !this.t.isStaticBlock(def)
|
|
||||||
)
|
|
||||||
.map(def => (def?.key?.name ? def.key.name : null));
|
|
||||||
|
|
||||||
for (const memberOrMethod of classBodyNode.body) {
|
|
||||||
classPath.scope.traverse(memberOrMethod, {
|
|
||||||
Identifier: (path: NodePath<t.Identifier>) => {
|
|
||||||
const idNode = path.node;
|
|
||||||
if ('key' in memberOrMethod && idNode === memberOrMethod.key) return;
|
|
||||||
const idName = idNode.name;
|
|
||||||
if (
|
|
||||||
availPropNames.includes(idName) &&
|
|
||||||
!this.isMemberExpression(path) &&
|
|
||||||
!this.isVariableDeclarator(path) &&
|
|
||||||
!this.isAttrFromFunction(path, idName, memberOrMethod) &&
|
|
||||||
!this.isObjectKey(path)
|
|
||||||
) {
|
|
||||||
path.replaceWith(this.t.memberExpression(this.t.thisExpression(), this.t.identifier(idName)));
|
|
||||||
path.skip();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if the identifier is from a function param, e.g:
|
|
||||||
* class MyClass {
|
|
||||||
* ok = 1
|
|
||||||
* myFunc1 = () => ok // change to myFunc1 = () => this.ok
|
|
||||||
* myFunc2 = ok => ok // don't change !!!!
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
isAttrFromFunction(path: NodePath<t.Identifier>, idName: string, stopNode: t.ClassBody['body'][number]) {
|
|
||||||
let reversePath = path.parentPath;
|
|
||||||
|
|
||||||
const checkParam = (param: t.Node): boolean => {
|
|
||||||
// ---- 3 general types:
|
|
||||||
// * represent allow nesting
|
|
||||||
// ---0 Identifier: (a)
|
|
||||||
// ---1 RestElement: (...a) *
|
|
||||||
// ---1 Pattern: 3 sub Pattern
|
|
||||||
// -----0 AssignmentPattern: (a=1) *
|
|
||||||
// -----1 ArrayPattern: ([a, b]) *
|
|
||||||
// -----2 ObjectPattern: ({a, b})
|
|
||||||
if (this.t.isIdentifier(param)) return param.name === idName;
|
|
||||||
if (this.t.isAssignmentPattern(param)) return checkParam(param.left);
|
|
||||||
if (this.t.isArrayPattern(param)) {
|
|
||||||
return param.elements.map(el => checkParam(el)).includes(true);
|
|
||||||
}
|
|
||||||
if (this.t.isObjectPattern(param)) {
|
|
||||||
return param.properties.map((prop: any) => prop.key.name).includes(idName);
|
|
||||||
}
|
|
||||||
if (this.t.isRestElement(param)) return checkParam(param.argument);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
while (reversePath && reversePath.node !== stopNode) {
|
|
||||||
const node = reversePath.node;
|
|
||||||
if (this.t.isArrowFunctionExpression(node) || this.t.isFunctionDeclaration(node)) {
|
|
||||||
for (const param of node.params) {
|
|
||||||
if (checkParam(param)) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reversePath = reversePath.parentPath;
|
|
||||||
}
|
|
||||||
if (this.t.isClassMethod(stopNode)) {
|
|
||||||
for (const param of stopNode.params) {
|
|
||||||
if (checkParam(param)) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if the identifier is already like `this.a` / `xx.a` but not like `a.xx` / xx[a]
|
|
||||||
*/
|
|
||||||
isMemberExpression(path: NodePath<t.Identifier>) {
|
|
||||||
const parentNode = path.parentPath.node;
|
|
||||||
return this.t.isMemberExpression(parentNode) && parentNode.property === path.node && !parentNode.computed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if the identifier is a variable declarator like `let a = 1` `for (let a in array)`
|
|
||||||
*/
|
|
||||||
isVariableDeclarator(path: NodePath<t.Identifier>) {
|
|
||||||
const parentNode = path.parentPath.node;
|
|
||||||
return this.t.isVariableDeclarator(parentNode) && parentNode.id === path.node;
|
|
||||||
}
|
|
||||||
|
|
||||||
isObjectKey(path: NodePath<t.Identifier>) {
|
|
||||||
const parentNode = path.parentPath.node;
|
|
||||||
return this.t.isObjectProperty(parentNode) && parentNode.key === path.node;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export interface Option {
|
|
||||||
files?: string | string[];
|
|
||||||
excludeFiles?: string | string[];
|
|
||||||
htmlTags?: string[];
|
|
||||||
parseTemplate?: boolean;
|
|
||||||
attributeMap?: Record<string, string>;
|
|
||||||
}
|
|
|
@ -388,7 +388,7 @@ export class ViewParser {
|
||||||
/**
|
/**
|
||||||
* @brief Collect all the mutable nodes in a static HTMLUnit
|
* @brief Collect all the mutable nodes in a static HTMLUnit
|
||||||
* We use this function to collect mutable nodes' path and props,
|
* We use this function to collect mutable nodes' path and props,
|
||||||
* so that in the generator, we know which position to insert the mutable nodes
|
* so that in the generate, we know which position to insert the mutable nodes
|
||||||
* @param htmlUnit
|
* @param htmlUnit
|
||||||
* @returns mutable particles
|
* @returns mutable particles
|
||||||
*/
|
*/
|
||||||
|
@ -435,7 +435,7 @@ export class ViewParser {
|
||||||
const templateProps: TemplateProp[] = [];
|
const templateProps: TemplateProp[] = [];
|
||||||
const generateVariableProp = (unit: HTMLUnit, path: number[]) => {
|
const generateVariableProp = (unit: HTMLUnit, path: number[]) => {
|
||||||
// ---- Generate all non-static(string/number/boolean) props for current HTMLUnit
|
// ---- Generate all non-static(string/number/boolean) props for current HTMLUnit
|
||||||
// to be inserted further in the generator
|
// to be inserted further in the generate
|
||||||
unit.props &&
|
unit.props &&
|
||||||
Object.entries(unit.props)
|
Object.entries(unit.props)
|
||||||
.filter(([, prop]) => !this.isStaticProp(prop))
|
.filter(([, prop]) => !this.isStaticProp(prop))
|
||||||
|
@ -489,7 +489,7 @@ export class ViewParser {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Filter out some props that are not needed in the template,
|
* @brief Filter out some props that are not needed in the template,
|
||||||
* these are all special props to be parsed differently in the generator
|
* these are all special props to be parsed differently in the generate
|
||||||
* @param props
|
* @param props
|
||||||
* @returns filtered props
|
* @returns filtered props
|
||||||
*/
|
*/
|
||||||
|
@ -539,7 +539,8 @@ export class ViewParser {
|
||||||
// ---- Get array
|
// ---- Get array
|
||||||
const arrayContainer = this.findProp(node, 'array');
|
const arrayContainer = this.findProp(node, 'array');
|
||||||
if (!arrayContainer) throw new Error('Missing [array] prop in for loop');
|
if (!arrayContainer) throw new Error('Missing [array] prop in for loop');
|
||||||
if (!this.t.isJSXExpressionContainer(arrayContainer.value)) throw new Error('Expected expression container for [array] prop');
|
if (!this.t.isJSXExpressionContainer(arrayContainer.value))
|
||||||
|
throw new Error('Expected expression container for [array] prop');
|
||||||
const array = arrayContainer.value.expression;
|
const array = arrayContainer.value.expression;
|
||||||
if (this.t.isJSXEmptyExpression(array)) throw new Error('Expected [array] expression not empty');
|
if (this.t.isJSXEmptyExpression(array)) throw new Error('Expected [array] expression not empty');
|
||||||
|
|
||||||
|
@ -555,17 +556,18 @@ export class ViewParser {
|
||||||
// ---- Get Item
|
// ---- Get Item
|
||||||
const itemProp = this.findProp(node, 'item');
|
const itemProp = this.findProp(node, 'item');
|
||||||
if (!itemProp) throw new Error('Missing [item] prop in for loop');
|
if (!itemProp) throw new Error('Missing [item] prop in for loop');
|
||||||
if (!this.t.isJSXExpressionContainer(itemProp.value)) throw new Error('Expected expression container for [item] prop');
|
if (!this.t.isJSXExpressionContainer(itemProp.value))
|
||||||
|
throw new Error('Expected expression container for [item] prop');
|
||||||
const item = itemProp.value.expression;
|
const item = itemProp.value.expression;
|
||||||
if (this.t.isJSXEmptyExpression(item)) throw new Error('Expected [item] expression not empty');
|
if (this.t.isJSXEmptyExpression(item)) throw new Error('Expected [item] expression not empty');
|
||||||
// ---- ObjectExpression to ObjectPattern / ArrayExpression to ArrayPattern
|
// ---- ObjectExpression to ObjectPattern / ArrayExpression to ArrayPattern
|
||||||
this.traverse(this.wrapWithFile(item), {
|
this.traverse(this.wrapWithFile(item), {
|
||||||
ObjectExpression: (path) => {
|
ObjectExpression: path => {
|
||||||
path.node.type = 'ObjectPattern' as any;
|
path.node.type = 'ObjectPattern' as any;
|
||||||
},
|
},
|
||||||
ArrayExpression: (path) => {
|
ArrayExpression: path => {
|
||||||
path.node.type = 'ArrayPattern' as any;
|
path.node.type = 'ArrayPattern' as any;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- Get children
|
// ---- Get children
|
||||||
|
@ -576,7 +578,7 @@ export class ViewParser {
|
||||||
key,
|
key,
|
||||||
item: item as t.LVal,
|
item: item as t.LVal,
|
||||||
array,
|
array,
|
||||||
children: this.parseView(children)
|
children: this.parseView(children),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openinula/error-handler": "workspace:*",
|
"@openinula/error-handler": "workspace:*",
|
||||||
"@openinula/jsx-view-parser": "workspace:*",
|
"@openinula/jsx-view-parser": "workspace:*",
|
||||||
|
"@openinula/babel-api": "workspace:*",
|
||||||
"@openinula/view-parser": "workspace:*"
|
"@openinula/view-parser": "workspace:*"
|
||||||
},
|
},
|
||||||
"tsup": {
|
"tsup": {
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
/*
|
||||||
|
* 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 { getBabelApi, types as t } from '@openinula/babel-api';
|
||||||
|
import { Bitmap } from './types';
|
||||||
|
|
||||||
|
export type Dependency = {
|
||||||
|
dependenciesNode: t.ArrayExpression;
|
||||||
|
/**
|
||||||
|
* Only contains the bit of direct dependencies and not contains the bit of used variables
|
||||||
|
* So it's configured in pruneUnusedBit.ts
|
||||||
|
*/
|
||||||
|
depMask?: Bitmap;
|
||||||
|
/**
|
||||||
|
* The bitmap of each dependency
|
||||||
|
*/
|
||||||
|
depBitmaps: Bitmap[];
|
||||||
|
fullDepMask: Bitmap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get all valid dependencies of a babel path
|
||||||
|
* @returns
|
||||||
|
* @param node
|
||||||
|
* @param reactiveMap
|
||||||
|
* @param reactivityFuncNames
|
||||||
|
*/
|
||||||
|
export function getDependenciesFromNode(
|
||||||
|
node: t.Expression | t.Statement,
|
||||||
|
reactiveMap: Map<string, number>,
|
||||||
|
reactivityFuncNames: string[]
|
||||||
|
): Dependency {
|
||||||
|
// ---- Deps: console.log(count)
|
||||||
|
const depBitmaps: number[] = [];
|
||||||
|
// ---- Assign deps: count = 1 or count++
|
||||||
|
let assignDepMask = 0;
|
||||||
|
const depNodes: Record<string, t.Node[]> = {};
|
||||||
|
const wrappedNode = valueWrapper(node);
|
||||||
|
|
||||||
|
getBabelApi().traverse(wrappedNode, {
|
||||||
|
Identifier: (innerPath: NodePath<t.Identifier>) => {
|
||||||
|
const propertyKey = innerPath.node.name;
|
||||||
|
const reactiveBitmap = reactiveMap.get(propertyKey);
|
||||||
|
|
||||||
|
if (reactiveBitmap !== undefined) {
|
||||||
|
if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath, reactivityFuncNames)) {
|
||||||
|
assignDepMask |= reactiveBitmap;
|
||||||
|
} else {
|
||||||
|
depBitmaps.push(reactiveBitmap);
|
||||||
|
|
||||||
|
if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
|
||||||
|
depNodes[propertyKey].push(geneDependencyNode(innerPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fullDepMask = depBitmaps.reduce((acc, cur) => acc | cur, 0);
|
||||||
|
// ---- 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
|
||||||
|
if (assignDepMask & fullDepMask) {
|
||||||
|
// TODO: We should throw an error here to indicate the user that there is a loop
|
||||||
|
}
|
||||||
|
|
||||||
|
// deduplicate the dependency nodes
|
||||||
|
let dependencyNodes = Object.values(depNodes).flat();
|
||||||
|
// ---- deduplicate the dependency nodes
|
||||||
|
dependencyNodes = dependencyNodes.filter((n, i) => {
|
||||||
|
const idx = dependencyNodes.findIndex(m => t.isNodesEquivalent(m, n));
|
||||||
|
return idx === i;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
dependenciesNode: t.arrayExpression(dependencyNodes as t.Expression[]),
|
||||||
|
depBitmaps,
|
||||||
|
fullDepMask,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
* @param reactivityFuncNames
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function isAssignmentFunction(innerPath: NodePath, reactivityFuncNames: string[]): 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 valueWrapper(node: t.Expression | t.Statement): t.File {
|
||||||
|
return t.file(t.program([t.isStatement(node) ? node : t.expressionStatement(node)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Generate a dependency node from a dependency identifier,
|
||||||
|
* loop until the parent node is not a binary expression or a member expression
|
||||||
|
* And turn the member expression into an optional member expression, like info.name -> info?.name
|
||||||
|
* @param path
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function geneDependencyNode(path: NodePath): t.Node {
|
||||||
|
let parentPath = path;
|
||||||
|
while (parentPath?.parentPath) {
|
||||||
|
const pParentPath = parentPath.parentPath;
|
||||||
|
if (
|
||||||
|
!(t.isMemberExpression(pParentPath.node, { computed: false }) || t.isOptionalMemberExpression(pParentPath.node))
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parentPath = pParentPath;
|
||||||
|
}
|
||||||
|
const depNode = t.cloneNode(parentPath.node);
|
||||||
|
// ---- Turn memberExpression to optionalMemberExpression
|
||||||
|
getBabelApi().traverse(valueWrapper(depNode as t.Expression), {
|
||||||
|
MemberExpression: innerPath => {
|
||||||
|
if (t.isThisExpression(innerPath.node.object)) return;
|
||||||
|
innerPath.node.optional = true;
|
||||||
|
innerPath.node.type = 'OptionalMemberExpression' as any;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return depNode;
|
||||||
|
}
|
|
@ -8,22 +8,18 @@ import { type ViewParticle, type ReactivityParserConfig } from './types';
|
||||||
* @param config
|
* @param config
|
||||||
* @returns [viewParticles, usedProperties]
|
* @returns [viewParticles, usedProperties]
|
||||||
*/
|
*/
|
||||||
export function parseReactivity(
|
export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserConfig): [ViewParticle[], number] {
|
||||||
viewUnits: ViewUnit[],
|
|
||||||
config: ReactivityParserConfig
|
|
||||||
): [ViewParticle[], Set<string>, number] {
|
|
||||||
// ---- ReactivityParser only accepts one view unit at a time,
|
// ---- ReactivityParser only accepts one view unit at a time,
|
||||||
// so we loop through the view units and get all the used properties
|
// so we loop through the view units and get all the used properties
|
||||||
const usedProperties = new Set<string>();
|
|
||||||
let usedBit = 0;
|
let usedBit = 0;
|
||||||
const dlParticles = viewUnits.map(viewUnit => {
|
const dlParticles = viewUnits.map(viewUnit => {
|
||||||
const parser = new ReactivityParser(config);
|
const parser = new ReactivityParser(config);
|
||||||
const dlParticle = parser.parse(viewUnit);
|
const dlParticle = parser.parse(viewUnit);
|
||||||
parser.usedProperties.forEach(usedProperties.add.bind(usedProperties));
|
|
||||||
usedBit |= parser.usedBit;
|
usedBit |= parser.usedBit;
|
||||||
return dlParticle;
|
return dlParticle;
|
||||||
});
|
});
|
||||||
return [dlParticles, usedProperties, usedBit];
|
return [dlParticles, usedBit];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type * from './types';
|
export { getDependenciesFromNode } from './getDependencies';
|
||||||
|
export * from './types';
|
||||||
|
|
|
@ -1,44 +1,42 @@
|
||||||
import {
|
import {
|
||||||
type TemplateProp,
|
|
||||||
type ReactivityParserConfig,
|
|
||||||
type MutableParticle,
|
|
||||||
type ViewParticle,
|
|
||||||
type TemplateParticle,
|
|
||||||
type TextParticle,
|
|
||||||
type HTMLParticle,
|
|
||||||
type DependencyProp,
|
|
||||||
type ExpParticle,
|
|
||||||
type CompParticle,
|
type CompParticle,
|
||||||
type ForParticle,
|
type DependencyProp,
|
||||||
type IfParticle,
|
|
||||||
type EnvParticle,
|
|
||||||
DepMaskMap,
|
DepMaskMap,
|
||||||
|
type EnvParticle,
|
||||||
|
type ExpParticle,
|
||||||
|
type ForParticle,
|
||||||
|
type HTMLParticle,
|
||||||
|
type IfParticle,
|
||||||
|
type MutableParticle,
|
||||||
|
type ReactivityParserConfig,
|
||||||
|
type TemplateParticle,
|
||||||
|
type TemplateProp,
|
||||||
|
type TextParticle,
|
||||||
|
type ViewParticle,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { type NodePath, type types as t, type traverse } from '@babel/core';
|
import { type NodePath, type traverse, type types as t } from '@babel/core';
|
||||||
import {
|
import {
|
||||||
type TextUnit,
|
|
||||||
type HTMLUnit,
|
|
||||||
type ViewUnit,
|
|
||||||
type CompUnit,
|
type CompUnit,
|
||||||
type UnitProp,
|
|
||||||
type ForUnit,
|
|
||||||
type IfUnit,
|
|
||||||
type EnvUnit,
|
type EnvUnit,
|
||||||
type ExpUnit,
|
type ExpUnit,
|
||||||
|
type ForUnit,
|
||||||
|
type HTMLUnit,
|
||||||
|
type IfUnit,
|
||||||
|
type TextUnit,
|
||||||
|
type UnitProp,
|
||||||
|
type ViewUnit,
|
||||||
} from '@openinula/jsx-view-parser';
|
} from '@openinula/jsx-view-parser';
|
||||||
import { DLError } from './error';
|
import { DLError } from './error';
|
||||||
|
import { getDependenciesFromNode } from './getDependencies';
|
||||||
|
|
||||||
export class ReactivityParser {
|
export class ReactivityParser {
|
||||||
private readonly config: ReactivityParserConfig;
|
private readonly config: ReactivityParserConfig;
|
||||||
|
|
||||||
private readonly t: typeof t;
|
private readonly t: typeof t;
|
||||||
private readonly traverse: typeof traverse;
|
private readonly traverse: typeof traverse;
|
||||||
private readonly availableProperties: string[];
|
|
||||||
private readonly depMaskMap: DepMaskMap;
|
private readonly depMaskMap: DepMaskMap;
|
||||||
private readonly identifierDepMap: Record<string, string[]>;
|
|
||||||
private readonly reactivityFuncNames;
|
private readonly reactivityFuncNames;
|
||||||
|
|
||||||
private readonly escapeNamings = ['escape', '$'];
|
|
||||||
private static readonly customHTMLProps = [
|
private static readonly customHTMLProps = [
|
||||||
'didUpdate',
|
'didUpdate',
|
||||||
'willMount',
|
'willMount',
|
||||||
|
@ -66,7 +64,6 @@ export class ReactivityParser {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.t = config.babelApi.types;
|
this.t = config.babelApi.types;
|
||||||
this.traverse = config.babelApi.traverse;
|
this.traverse = config.babelApi.traverse;
|
||||||
this.availableProperties = config.availableProperties;
|
|
||||||
this.depMaskMap = config.depMaskMap;
|
this.depMaskMap = config.depMaskMap;
|
||||||
this.reactivityFuncNames = config.reactivityFuncNames ?? [];
|
this.reactivityFuncNames = config.reactivityFuncNames ?? [];
|
||||||
}
|
}
|
||||||
|
@ -162,7 +159,7 @@ export class ReactivityParser {
|
||||||
/**
|
/**
|
||||||
* @brief Collect all the mutable nodes in a static HTMLUnit
|
* @brief Collect all the mutable nodes in a static HTMLUnit
|
||||||
* We use this function to collect mutable nodes' path and props,
|
* We use this function to collect mutable nodes' path and props,
|
||||||
* so that in the generator, we know which position to insert the mutable nodes
|
* so that in the generate, we know which position to insert the mutable nodes
|
||||||
* @param htmlUnit
|
* @param htmlUnit
|
||||||
* @returns mutable particles
|
* @returns mutable particles
|
||||||
*/
|
*/
|
||||||
|
@ -204,7 +201,7 @@ export class ReactivityParser {
|
||||||
const templateProps: TemplateProp[] = [];
|
const templateProps: TemplateProp[] = [];
|
||||||
const generateVariableProp = (unit: HTMLUnit, path: number[]) => {
|
const generateVariableProp = (unit: HTMLUnit, path: number[]) => {
|
||||||
// ---- Generate all non-static(string/number/boolean) props for current HTMLUnit
|
// ---- Generate all non-static(string/number/boolean) props for current HTMLUnit
|
||||||
// to be inserted further in the generator
|
// to be inserted further in the generate
|
||||||
Object.entries(unit.props)
|
Object.entries(unit.props)
|
||||||
.filter(([, prop]) => !this.isStaticProp(prop))
|
.filter(([, prop]) => !this.isStaticProp(prop))
|
||||||
.forEach(([key, prop]) => {
|
.forEach(([key, prop]) => {
|
||||||
|
@ -235,6 +232,7 @@ export class ReactivityParser {
|
||||||
value: child.content,
|
value: child.content,
|
||||||
depMask: 0,
|
depMask: 0,
|
||||||
dependenciesNode: this.t.arrayExpression([]),
|
dependenciesNode: this.t.arrayExpression([]),
|
||||||
|
depBitmaps: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -341,6 +339,7 @@ export class ReactivityParser {
|
||||||
item: forUnit.item,
|
item: forUnit.item,
|
||||||
array: {
|
array: {
|
||||||
value: forUnit.array,
|
value: forUnit.array,
|
||||||
|
depBitmaps: [],
|
||||||
depMask: depMask,
|
depMask: depMask,
|
||||||
dependenciesNode,
|
dependenciesNode,
|
||||||
},
|
},
|
||||||
|
@ -393,14 +392,10 @@ export class ReactivityParser {
|
||||||
* @returns ExpParticle
|
* @returns ExpParticle
|
||||||
*/
|
*/
|
||||||
private parseExp(expUnit: ExpUnit): ExpParticle {
|
private parseExp(expUnit: ExpUnit): ExpParticle {
|
||||||
const expParticle: ExpParticle = {
|
return {
|
||||||
type: 'exp',
|
type: 'exp',
|
||||||
content: this.generateDependencyProp(expUnit.content),
|
content: this.generateDependencyProp(expUnit.content),
|
||||||
props: Object.fromEntries(
|
|
||||||
Object.entries(expUnit.props).map(([key, prop]) => [key, this.generateDependencyProp(prop)])
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
return expParticle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Dependencies ----
|
// ---- Dependencies ----
|
||||||
|
@ -410,14 +405,13 @@ export class ReactivityParser {
|
||||||
* @returns DependencyProp
|
* @returns DependencyProp
|
||||||
*/
|
*/
|
||||||
private generateDependencyProp(prop: UnitProp): DependencyProp {
|
private generateDependencyProp(prop: UnitProp): DependencyProp {
|
||||||
const dependencyProp: DependencyProp = {
|
return {
|
||||||
value: prop.value,
|
value: prop.value,
|
||||||
...this.getDependencies(prop.value),
|
...this.getDependencies(prop.value),
|
||||||
viewPropMap: Object.fromEntries(
|
viewPropMap: Object.fromEntries(
|
||||||
Object.entries(prop.viewPropMap).map(([key, units]) => [key, units.map(this.parseViewParticle.bind(this))])
|
Object.entries(prop.viewPropMap).map(([key, units]) => [key, units.map(this.parseViewParticle.bind(this))])
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
return dependencyProp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -430,149 +424,20 @@ export class ReactivityParser {
|
||||||
* @param node
|
* @param node
|
||||||
* @returns dependency index array
|
* @returns dependency index array
|
||||||
*/
|
*/
|
||||||
private getDependencies(node: t.Expression | t.Statement): {
|
private getDependencies(node: t.Expression | t.Statement) {
|
||||||
depMask: number;
|
|
||||||
dependenciesNode: t.ArrayExpression;
|
|
||||||
} {
|
|
||||||
if (this.t.isFunctionExpression(node) || this.t.isArrowFunctionExpression(node)) {
|
if (this.t.isFunctionExpression(node) || this.t.isArrowFunctionExpression(node)) {
|
||||||
return {
|
return {
|
||||||
depMask: 0,
|
depMask: 0,
|
||||||
|
depBitmaps: [],
|
||||||
dependenciesNode: this.t.arrayExpression([]),
|
dependenciesNode: this.t.arrayExpression([]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// ---- 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 [deps, propertyDepNodes] = this.getPropertyDependencies(node);
|
const dependency = getDependenciesFromNode(node, this.depMaskMap, this.reactivityFuncNames);
|
||||||
|
this.usedBit |= dependency.fullDepMask;
|
||||||
const depNodes = [...propertyDepNodes] as t.Expression[];
|
return dependency;
|
||||||
|
|
||||||
return {
|
|
||||||
depMask: deps,
|
|
||||||
dependenciesNode: this.t.arrayExpression(depNodes),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get all the dependencies of a node if a member expression is a valid dependency as
|
|
||||||
* 1. the property is in the availableProperties
|
|
||||||
* 2. the object is this
|
|
||||||
* 3. the member expression is not in an escape function
|
|
||||||
* 4. the member expression is not in a manual function
|
|
||||||
* 5. the member expression is not the left side of an assignment expression, which is an assignment expression
|
|
||||||
* 6. the member is not a pure function declaration
|
|
||||||
* @param node
|
|
||||||
* @returns dependency index array
|
|
||||||
*/
|
|
||||||
private getPropertyDependencies(node: t.Expression | t.Statement): [number, t.Node[]] {
|
|
||||||
// ---- Deps: console.log(count)
|
|
||||||
let depMask = 0;
|
|
||||||
// ---- Assign deps: count = 1 or count++
|
|
||||||
let assignDepBit = 0;
|
|
||||||
const depNodes: Record<string, t.Node[]> = {};
|
|
||||||
const deps = new Set<string>();
|
|
||||||
|
|
||||||
const wrappedNode = this.valueWrapper(node);
|
|
||||||
this.traverse(wrappedNode, {
|
|
||||||
Identifier: innerPath => {
|
|
||||||
const propertyKey = innerPath.node.name;
|
|
||||||
const reactiveBitmap = this.depMaskMap.get(propertyKey);
|
|
||||||
|
|
||||||
if (reactiveBitmap !== undefined) {
|
|
||||||
if (this.isAssignmentExpressionLeft(innerPath) || this.isAssignmentFunction(innerPath)) {
|
|
||||||
assignDepBit |= reactiveBitmap;
|
|
||||||
} else {
|
|
||||||
depMask |= reactiveBitmap;
|
|
||||||
deps.add(propertyKey);
|
|
||||||
|
|
||||||
if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
|
|
||||||
depNodes[propertyKey].push(this.geneDependencyNode(innerPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- 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
|
|
||||||
if (assignDepBit & depMask) {
|
|
||||||
// TODO: I think we should throw an error here to indicate the user that there is a loop
|
|
||||||
}
|
|
||||||
|
|
||||||
let dependencyNodes = Object.values(depNodes).flat();
|
|
||||||
// ---- deduplicate the dependency nodes
|
|
||||||
dependencyNodes = dependencyNodes.filter((n, i) => {
|
|
||||||
const idx = dependencyNodes.findIndex(m => this.t.isNodesEquivalent(m, n));
|
|
||||||
return idx === i;
|
|
||||||
});
|
|
||||||
|
|
||||||
deps.forEach(this.usedProperties.add.bind(this.usedProperties));
|
|
||||||
this.usedBit |= depMask;
|
|
||||||
return [depMask, dependencyNodes];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Generate a dependency node from a dependency identifier,
|
|
||||||
* loop until the parent node is not a binary expression or a member expression
|
|
||||||
* And turn the member expression into an optional member expression, like info.name -> info?.name
|
|
||||||
* @param path
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
private geneDependencyNode(path: NodePath): t.Node {
|
|
||||||
let parentPath = path;
|
|
||||||
while (parentPath?.parentPath) {
|
|
||||||
const pParentPath = parentPath.parentPath;
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
this.t.isMemberExpression(pParentPath.node, { computed: false }) ||
|
|
||||||
this.t.isOptionalMemberExpression(pParentPath.node)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
parentPath = pParentPath;
|
|
||||||
}
|
|
||||||
const depNode = this.t.cloneNode(parentPath.node);
|
|
||||||
// ---- Turn memberExpression to optionalMemberExpression
|
|
||||||
this.traverse(this.valueWrapper(depNode as t.Expression), {
|
|
||||||
MemberExpression: innerPath => {
|
|
||||||
if (this.t.isThisExpression(innerPath.node.object)) return;
|
|
||||||
innerPath.node.optional = true;
|
|
||||||
innerPath.node.type = 'OptionalMemberExpression' as any;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return depNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get dependencies from the identifierDepMap
|
|
||||||
* e.g.
|
|
||||||
* map: { "a": ["dep1", "dep2"] }
|
|
||||||
* expression: const b = a
|
|
||||||
* deps for b: ["dep1", "dep2"]
|
|
||||||
* @param node
|
|
||||||
* @returns dependency index array
|
|
||||||
*/
|
|
||||||
private getIdentifierMapDependencies(node: t.Expression | t.Statement): number[] {
|
|
||||||
const deps = new Set<string>();
|
|
||||||
|
|
||||||
const wrappedNode = this.valueWrapper(node);
|
|
||||||
this.traverse(wrappedNode, {
|
|
||||||
Identifier: innerPath => {
|
|
||||||
const identifier = innerPath.node;
|
|
||||||
const idName = identifier.name;
|
|
||||||
if (this.isAttrFromFunction(innerPath, idName)) return;
|
|
||||||
const depsArray = this.identifierDepMap[idName];
|
|
||||||
|
|
||||||
if (!depsArray || !Array.isArray(depsArray)) return;
|
|
||||||
if (this.isMemberInEscapeFunction(innerPath) || this.isMemberInManualFunction(innerPath)) return;
|
|
||||||
depsArray.forEach(deps.add.bind(deps));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
deps.forEach(this.usedProperties.add.bind(this.usedProperties));
|
|
||||||
return [...deps].map(dep => this.availableProperties.lastIndexOf(dep));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Utils ----
|
// ---- Utils ----
|
||||||
|
@ -626,7 +491,7 @@ export class ReactivityParser {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Filter out some props that are not needed in the template,
|
* @brief Filter out some props that are not needed in the template,
|
||||||
* these are all special props to be parsed differently in the generator
|
* these are all special props to be parsed differently in the generate
|
||||||
* @param props
|
* @param props
|
||||||
* @returns filtered props
|
* @returns filtered props
|
||||||
*/
|
*/
|
||||||
|
@ -750,112 +615,4 @@ export class ReactivityParser {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if it's the left side of an assignment expression, e.g. this.count = 1
|
|
||||||
* @param innerPath
|
|
||||||
* @returns is left side of an assignment expression
|
|
||||||
*/
|
|
||||||
private isAssignmentExpressionLeft(innerPath: NodePath): boolean {
|
|
||||||
let parentPath = innerPath.parentPath;
|
|
||||||
while (parentPath && !this.t.isStatement(parentPath.node)) {
|
|
||||||
if (this.t.isAssignmentExpression(parentPath.node)) {
|
|
||||||
if (parentPath.node.left === innerPath.node) return true;
|
|
||||||
const leftPath = parentPath.get('left') as NodePath;
|
|
||||||
if (innerPath.isDescendant(leftPath)) return true;
|
|
||||||
} else if (this.t.isUpdateExpression(parentPath.node)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
parentPath = parentPath.parentPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if it's a reactivity function, e.g. arr.push
|
|
||||||
* @param innerPath
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
private isAssignmentFunction(innerPath: NodePath): boolean {
|
|
||||||
let parentPath = innerPath.parentPath;
|
|
||||||
|
|
||||||
while (parentPath && this.t.isMemberExpression(parentPath.node)) {
|
|
||||||
parentPath = parentPath.parentPath;
|
|
||||||
}
|
|
||||||
if (!parentPath) return false;
|
|
||||||
return (
|
|
||||||
parentPath.isCallExpression() &&
|
|
||||||
parentPath.get('callee').isIdentifier() &&
|
|
||||||
this.reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if it's in an "escape" function,
|
|
||||||
* e.g. escape(() => { console.log(this.count) })
|
|
||||||
* deps will be empty instead of ["count"]
|
|
||||||
* @param innerPath
|
|
||||||
* @param classDeclarationNode
|
|
||||||
* @returns is in escape function
|
|
||||||
*/
|
|
||||||
private isMemberInEscapeFunction(innerPath: NodePath): boolean {
|
|
||||||
let isInFunction = false;
|
|
||||||
let reversePath = innerPath.parentPath;
|
|
||||||
while (reversePath) {
|
|
||||||
const node = reversePath.node;
|
|
||||||
if (
|
|
||||||
this.t.isCallExpression(node) &&
|
|
||||||
this.t.isIdentifier(node.callee) &&
|
|
||||||
this.escapeNamings.includes(node.callee.name)
|
|
||||||
) {
|
|
||||||
isInFunction = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reversePath = reversePath.parentPath;
|
|
||||||
}
|
|
||||||
return isInFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if it's in a "manual" function,
|
|
||||||
* e.g. manual(() => { console.log(this.count) }, ["flag"])
|
|
||||||
* deps will be ["flag"] instead of ["count"]
|
|
||||||
* @param innerPath
|
|
||||||
* @param classDeclarationNode
|
|
||||||
* @returns is in manual function
|
|
||||||
*/
|
|
||||||
private isMemberInManualFunction(innerPath: NodePath): boolean {
|
|
||||||
let isInFunction = false;
|
|
||||||
let reversePath = innerPath.parentPath;
|
|
||||||
|
|
||||||
while (reversePath) {
|
|
||||||
const node = reversePath.node;
|
|
||||||
const parentNode = reversePath.parentPath?.node;
|
|
||||||
const isManual =
|
|
||||||
this.t.isCallExpression(parentNode) &&
|
|
||||||
this.t.isIdentifier(parentNode.callee) &&
|
|
||||||
parentNode.callee.name === 'manual';
|
|
||||||
const isFirstParam = this.t.isCallExpression(parentNode) && parentNode.arguments[0] === node;
|
|
||||||
if (isManual && isFirstParam) {
|
|
||||||
isInFunction = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reversePath = reversePath.parentPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isInFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Generate a random string
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
private uid(): string {
|
|
||||||
return Math.random().toString(36).slice(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deduplicate<T>(arr: T[]): T[] {
|
|
||||||
return [...new Set(arr)];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,16 @@ import type Babel from '@babel/core';
|
||||||
|
|
||||||
export interface DependencyValue<T> {
|
export interface DependencyValue<T> {
|
||||||
value: T;
|
value: T;
|
||||||
depMask: number; // -> bit
|
depMask?: number; // -> bit
|
||||||
|
depBitmaps: number[];
|
||||||
dependenciesNode: t.ArrayExpression;
|
dependenciesNode: t.ArrayExpression;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DependencyProp {
|
export interface DependencyProp {
|
||||||
value: t.Expression;
|
value: t.Expression;
|
||||||
viewPropMap: Record<string, ViewParticle[]>;
|
viewPropMap: Record<string, ViewParticle[]>;
|
||||||
depMask: number;
|
depMask?: number;
|
||||||
|
depBitmaps: number[];
|
||||||
dependenciesNode: t.ArrayExpression;
|
dependenciesNode: t.ArrayExpression;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +21,8 @@ export interface TemplateProp {
|
||||||
key: string;
|
key: string;
|
||||||
path: number[];
|
path: number[];
|
||||||
value: t.Expression;
|
value: t.Expression;
|
||||||
depMask: number;
|
depMask?: number;
|
||||||
|
depBitmaps: number[];
|
||||||
dependenciesNode: t.ArrayExpression;
|
dependenciesNode: t.ArrayExpression;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +81,6 @@ export interface EnvParticle {
|
||||||
export interface ExpParticle {
|
export interface ExpParticle {
|
||||||
type: 'exp';
|
type: 'exp';
|
||||||
content: DependencyProp;
|
content: DependencyProp;
|
||||||
props: Record<string, DependencyProp>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewParticle =
|
export type ViewParticle =
|
||||||
|
@ -93,8 +95,6 @@ export type ViewParticle =
|
||||||
|
|
||||||
export interface ReactivityParserConfig {
|
export interface ReactivityParserConfig {
|
||||||
babelApi: typeof Babel;
|
babelApi: typeof Babel;
|
||||||
availableProperties: string[];
|
|
||||||
availableIdentifiers?: string[];
|
|
||||||
depMaskMap: DepMaskMap;
|
depMaskMap: DepMaskMap;
|
||||||
identifierDepMap?: Record<string, Bitmap>;
|
identifierDepMap?: Record<string, Bitmap>;
|
||||||
dependencyParseType?: 'property' | 'identifier';
|
dependencyParseType?: 'property' | 'identifier';
|
||||||
|
@ -103,5 +103,5 @@ export interface ReactivityParserConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: unify with the types in babel-inula-next-core
|
// TODO: unify with the types in babel-inula-next-core
|
||||||
type Bitmap = number;
|
export type Bitmap = number;
|
||||||
export type DepMaskMap = Map<string, Bitmap>;
|
export type DepMaskMap = Map<string, Bitmap>;
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@openinula/view-generator",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"author": {
|
|
||||||
"name": "IanDx",
|
|
||||||
"email": "iandxssxx@gmail.com"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"dlight.js"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.cjs",
|
|
||||||
"module": "dist/index.js",
|
|
||||||
"typings": "dist/index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsup --sourcemap"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.20.12",
|
|
||||||
"@types/babel__core": "^7.20.5",
|
|
||||||
"@types/node": "^20.10.5",
|
|
||||||
"tsup": "^6.7.0",
|
|
||||||
"typescript": "^5.3.2",
|
|
||||||
"@openinula/reactivity-parser": "workspace:*"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@openinula/error-handler": "workspace:*"
|
|
||||||
},
|
|
||||||
"tsup": {
|
|
||||||
"entry": [
|
|
||||||
"src/index.ts"
|
|
||||||
],
|
|
||||||
"format": [
|
|
||||||
"cjs",
|
|
||||||
"esm"
|
|
||||||
],
|
|
||||||
"clean": true,
|
|
||||||
"dts": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,284 +0,0 @@
|
||||||
import { type types as t, type traverse } from '@babel/core';
|
|
||||||
import { type ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
import { type SnippetPropMap, type ViewGeneratorConfig } from '../types';
|
|
||||||
import ViewGenerator from '../ViewGenerator';
|
|
||||||
|
|
||||||
export const prefixMap = { template: '$t', node: '$node' };
|
|
||||||
|
|
||||||
export default class BaseGenerator {
|
|
||||||
readonly viewParticle: ViewParticle;
|
|
||||||
readonly config: ViewGeneratorConfig;
|
|
||||||
|
|
||||||
readonly t: typeof t;
|
|
||||||
readonly traverse: typeof traverse;
|
|
||||||
readonly className: string;
|
|
||||||
readonly importMap: Record<string, string>;
|
|
||||||
readonly snippetPropMap: SnippetPropMap;
|
|
||||||
readonly elementAttributeMap;
|
|
||||||
readonly alterAttributeMap;
|
|
||||||
|
|
||||||
readonly viewGenerator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Constructor
|
|
||||||
* @param viewUnit
|
|
||||||
* @param config
|
|
||||||
*/
|
|
||||||
constructor(viewParticle: ViewParticle, config: ViewGeneratorConfig) {
|
|
||||||
this.viewParticle = viewParticle;
|
|
||||||
this.config = config;
|
|
||||||
this.t = config.babelApi.types;
|
|
||||||
this.traverse = config.babelApi.traverse;
|
|
||||||
this.className = config.className;
|
|
||||||
this.importMap = config.importMap;
|
|
||||||
this.snippetPropMap = config.snippetPropMap;
|
|
||||||
this.viewGenerator = new ViewGenerator(config);
|
|
||||||
this.elementAttributeMap = config.attributeMap
|
|
||||||
? Object.entries(config.attributeMap).reduce<Record<string, string[]>>((acc, [key, elements]) => {
|
|
||||||
elements.forEach(element => {
|
|
||||||
if (!acc[element]) acc[element] = [];
|
|
||||||
acc[element].push(key);
|
|
||||||
});
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
: {};
|
|
||||||
this.alterAttributeMap = config.alterAttributeMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Init Statements
|
|
||||||
private readonly initStatements: t.Statement[] = [];
|
|
||||||
addInitStatement(...statements: (t.Statement | null)[]) {
|
|
||||||
this.initStatements.push(...(statements.filter(Boolean) as t.Statement[]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Added Class Properties, typically used in for Template
|
|
||||||
private readonly classProperties: t.ClassProperty[] = [];
|
|
||||||
addStaticClassProperty(key: string, value: t.Expression) {
|
|
||||||
this.classProperties.push(
|
|
||||||
this.t.classProperty(this.t.identifier(key), value, undefined, undefined, undefined, true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Update Statements
|
|
||||||
private readonly updateStatements: Record<number, t.Statement[]> = {};
|
|
||||||
addUpdateStatements(dependencies: number[] | undefined, statement: t.Statement | undefined | null) {
|
|
||||||
if (!dependencies || dependencies.length === 0) return;
|
|
||||||
const depNum = BaseGenerator.calcDependencyNum(dependencies);
|
|
||||||
if (!this.updateStatements[depNum]) this.updateStatements[depNum] = [];
|
|
||||||
if (statement) this.updateStatements[depNum].push(statement);
|
|
||||||
}
|
|
||||||
|
|
||||||
addUpdateStatementsWithoutDep(statement: t.Statement) {
|
|
||||||
if (!this.updateStatements[0]) this.updateStatements[0] = [];
|
|
||||||
this.updateStatements[0].push(statement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns [initStatements, updateStatements, classProperties, nodeName]
|
|
||||||
*/
|
|
||||||
generate(): [t.Statement[], Record<number, t.Statement[]>, t.ClassProperty[], string] {
|
|
||||||
const nodeName = this.run();
|
|
||||||
return [this.initStatements, this.updateStatements, this.classProperties, nodeName];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Generate the view given the view particles, mainly used for child particles parsing
|
|
||||||
* @param viewParticles
|
|
||||||
* @param mergeStatements
|
|
||||||
* @returns [initStatements, topLevelNodes, updateStatements]
|
|
||||||
*/
|
|
||||||
generateChildren(
|
|
||||||
viewParticles: ViewParticle[],
|
|
||||||
mergeStatements = true,
|
|
||||||
newIdx = false
|
|
||||||
): [t.Statement[], string[], Record<number, t.Statement[]>, number] {
|
|
||||||
this.viewGenerator.nodeIdx = newIdx ? -1 : this.nodeIdx;
|
|
||||||
this.viewGenerator.templateIdx = this.templateIdx;
|
|
||||||
const [initStatements, updateStatements, classProperties, topLevelNodes] =
|
|
||||||
this.viewGenerator.generateChildren(viewParticles);
|
|
||||||
if (!newIdx) this.nodeIdx = this.viewGenerator.nodeIdx;
|
|
||||||
this.templateIdx = this.viewGenerator.templateIdx;
|
|
||||||
this.classProperties.push(...classProperties);
|
|
||||||
if (mergeStatements) this.mergeStatements(updateStatements);
|
|
||||||
|
|
||||||
return [initStatements, topLevelNodes, updateStatements, this.viewGenerator.nodeIdx];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Merge the update statements
|
|
||||||
* @param statements
|
|
||||||
*/
|
|
||||||
private mergeStatements(statements: Record<number, t.Statement[]>): void {
|
|
||||||
Object.entries(statements).forEach(([depNum, statements]) => {
|
|
||||||
if (!this.updateStatements[Number(depNum)]) {
|
|
||||||
this.updateStatements[Number(depNum)] = [];
|
|
||||||
}
|
|
||||||
this.updateStatements[Number(depNum)].push(...statements);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Generate the view given the view particle
|
|
||||||
* @param viewParticle
|
|
||||||
* @param mergeStatements
|
|
||||||
* @returns [initStatements, nodeName, updateStatements]
|
|
||||||
*/
|
|
||||||
generateChild(
|
|
||||||
viewParticle: ViewParticle,
|
|
||||||
mergeStatements = true,
|
|
||||||
newIdx = false
|
|
||||||
): [t.Statement[], string, Record<number, t.Statement[]>, number] {
|
|
||||||
this.viewGenerator.nodeIdx = newIdx ? -1 : this.nodeIdx;
|
|
||||||
this.viewGenerator.templateIdx = this.templateIdx;
|
|
||||||
const [initStatements, updateStatements, classProperties, nodeName] =
|
|
||||||
this.viewGenerator.generateChild(viewParticle);
|
|
||||||
if (!newIdx) this.nodeIdx = this.viewGenerator.nodeIdx;
|
|
||||||
this.templateIdx = this.viewGenerator.templateIdx;
|
|
||||||
this.classProperties.push(...classProperties);
|
|
||||||
if (mergeStatements) this.mergeStatements(updateStatements);
|
|
||||||
|
|
||||||
return [initStatements, nodeName, updateStatements, this.viewGenerator.nodeIdx];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* const $update = (changed) => { ${updateStatements} }
|
|
||||||
*/
|
|
||||||
geneUpdateFunc(updateStatements: Record<number, t.Statement[]>): t.Statement {
|
|
||||||
return this.t.variableDeclaration('const', [
|
|
||||||
this.t.variableDeclarator(
|
|
||||||
this.t.identifier('$update'),
|
|
||||||
this.t.arrowFunctionExpression([this.t.identifier('$changed')], this.geneUpdateBody(updateStatements))
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
get updateParams() {
|
|
||||||
return [this.t.identifier('$changed')];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* (changed) => {
|
|
||||||
* if (changed & 1) {
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
geneUpdateBody(updateStatements: Record<number, t.Statement[]>): t.BlockStatement {
|
|
||||||
return this.t.blockStatement([
|
|
||||||
...Object.entries(updateStatements)
|
|
||||||
.filter(([depNum]) => depNum !== '0')
|
|
||||||
.map(([depNum, statements]) => {
|
|
||||||
return this.t.ifStatement(
|
|
||||||
this.t.binaryExpression('&', this.t.identifier('$changed'), this.t.numericLiteral(Number(depNum))),
|
|
||||||
this.t.blockStatement(statements)
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
...(updateStatements[0] ?? []),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* let node1, node2, ...
|
|
||||||
*/
|
|
||||||
declareNodes(nodeIdx: number): t.VariableDeclaration[] {
|
|
||||||
if (nodeIdx === -1) return [];
|
|
||||||
return [
|
|
||||||
this.t.variableDeclaration(
|
|
||||||
'let',
|
|
||||||
Array.from({ length: nodeIdx + 1 }, (_, i) =>
|
|
||||||
this.t.variableDeclarator(this.t.identifier(`${prefixMap.node}${i}`))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* return [${topLevelNodes}]
|
|
||||||
*/
|
|
||||||
generateReturnStatement(topLevelNodes: string[]): t.ReturnStatement {
|
|
||||||
return this.t.returnStatement(this.t.arrayExpression(topLevelNodes.map(name => this.t.identifier(name))));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief To be implemented by the subclass as the main node generation function
|
|
||||||
* @returns dlNodeName
|
|
||||||
*/
|
|
||||||
run(): string {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Name ----
|
|
||||||
// ---- Used as dlNodeName for any node declaration
|
|
||||||
nodeIdx = -1;
|
|
||||||
generateNodeName(idx?: number): string {
|
|
||||||
return `${prefixMap.node}${idx ?? ++this.nodeIdx}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Used as template generation as class property
|
|
||||||
templateIdx = -1;
|
|
||||||
generateTemplateName(): string {
|
|
||||||
return `${prefixMap.template}${++this.templateIdx}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- @Utils -----
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param updateStatements
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Calculate the dependency number from an array of dependency index
|
|
||||||
* e.g.
|
|
||||||
* [0, 1, 2] => 0b111 => 7
|
|
||||||
* [1, 3] => 0b1010 => 10
|
|
||||||
* @param dependencies
|
|
||||||
* @returns dependency number
|
|
||||||
*/
|
|
||||||
static calcDependencyNum(dependencies: number[] | undefined): number {
|
|
||||||
if (!dependencies || dependencies.length === 0) return 0;
|
|
||||||
dependencies = [...new Set(dependencies)];
|
|
||||||
return dependencies.reduce((acc, dep) => acc + (1 << dep), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Wrap the value in a file
|
|
||||||
* @param node
|
|
||||||
* @returns wrapped value
|
|
||||||
*/
|
|
||||||
valueWrapper(node: t.Expression | t.Statement): t.File {
|
|
||||||
return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} && ${expression}
|
|
||||||
*/
|
|
||||||
optionalExpression(dlNodeName: string, expression: t.Expression): t.Statement {
|
|
||||||
return this.t.expressionStatement(this.t.logicalExpression('&&', this.t.identifier(dlNodeName), expression));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Shorthand function for collecting statements in batch
|
|
||||||
* @returns [statements, collect]
|
|
||||||
*/
|
|
||||||
static statementsCollector(): [t.Statement[], (...statements: t.Statement[] | t.Statement[][]) => void] {
|
|
||||||
const statements: t.Statement[] = [];
|
|
||||||
const collect = (...newStatements: t.Statement[] | t.Statement[][]) => {
|
|
||||||
newStatements.forEach(s => {
|
|
||||||
if (Array.isArray(s)) {
|
|
||||||
statements.push(...s);
|
|
||||||
} else {
|
|
||||||
statements.push(s);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return [statements, collect];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import BaseGenerator from './BaseGenerator';
|
|
||||||
|
|
||||||
export default class CondGenerator extends BaseGenerator {
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* $thisCond.cond = ${idx}
|
|
||||||
*/
|
|
||||||
geneCondIdx(idx: number): t.ExpressionStatement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')),
|
|
||||||
this.t.numericLiteral(idx)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* if ($thisCond.cond === ${idx}) {
|
|
||||||
* $thisCond.didntChange = true
|
|
||||||
* return []
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
geneCondCheck(idx: number): t.IfStatement {
|
|
||||||
return this.t.ifStatement(
|
|
||||||
this.t.binaryExpression(
|
|
||||||
'===',
|
|
||||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')),
|
|
||||||
this.t.numericLiteral(idx)
|
|
||||||
),
|
|
||||||
this.t.blockStatement([
|
|
||||||
this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('didntChange')),
|
|
||||||
this.t.booleanLiteral(true)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
this.t.returnStatement(this.t.arrayExpression([])),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}?.updateCond(key)
|
|
||||||
*/
|
|
||||||
updateCondNodeCond(dlNodeName: string): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('updateCond')), [
|
|
||||||
...this.updateParams.slice(1),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}?.update(changed)
|
|
||||||
*/
|
|
||||||
updateCondNode(dlNodeName: string): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(
|
|
||||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('update')),
|
|
||||||
this.updateParams
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} = new CondNode(${depNum}, ($thisCond) => {})
|
|
||||||
*/
|
|
||||||
declareCondNode(dlNodeName: string, condFunc: t.BlockStatement, deps: number[]): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.newExpression(this.t.identifier(this.importMap.CondNode), [
|
|
||||||
this.t.numericLiteral(CondGenerator.calcDependencyNum(deps)),
|
|
||||||
this.t.arrowFunctionExpression([this.t.identifier('$thisCond')], condFunc),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return $thisCond.cond === ${branchIdx} ? [${nodeNames}] : $thisCond.updateCond()
|
|
||||||
*/
|
|
||||||
geneCondReturnStatement(nodeNames: string[], branchIdx: number): t.Statement {
|
|
||||||
// ---- If the returned cond is not the last one,
|
|
||||||
// it means it's been altered in the childrenNodes,
|
|
||||||
// so we update the cond again to get the right one
|
|
||||||
return this.t.returnStatement(
|
|
||||||
this.t.conditionalExpression(
|
|
||||||
this.t.binaryExpression(
|
|
||||||
'===',
|
|
||||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')),
|
|
||||||
this.t.numericLiteral(branchIdx)
|
|
||||||
),
|
|
||||||
this.t.arrayExpression(nodeNames.map(name => this.t.identifier(name))),
|
|
||||||
this.t.callExpression(
|
|
||||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('updateCond')),
|
|
||||||
this.updateParams.slice(1)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import PropViewGenerator from './PropViewGenerator';
|
|
||||||
|
|
||||||
export default class ElementGenerator extends PropViewGenerator {
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* el:
|
|
||||||
* View.addDidMount(${dlNodeName}, () => (
|
|
||||||
* typeof ${value} === "function" ? ${value}($nodeEl) : ${value} = $nodeEl
|
|
||||||
* ))
|
|
||||||
* not el:
|
|
||||||
* typeof ${value} === "function" ? ${value}($nodeEl) : ${value} = $nodeEl
|
|
||||||
* @param el true: dlNodeName._$el, false: dlNodeName
|
|
||||||
*/
|
|
||||||
initElement(dlNodeName: string, value: t.Expression, el = false): t.Statement {
|
|
||||||
const elNode = el
|
|
||||||
? this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$el'))
|
|
||||||
: this.t.identifier(dlNodeName);
|
|
||||||
let elementNode;
|
|
||||||
if (this.isOnlyMemberExpression(value)) {
|
|
||||||
elementNode = this.t.conditionalExpression(
|
|
||||||
this.t.binaryExpression('===', this.t.unaryExpression('typeof', value, true), this.t.stringLiteral('function')),
|
|
||||||
this.t.callExpression(value, [elNode]),
|
|
||||||
this.t.assignmentExpression('=', value as t.LVal, elNode)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
elementNode = this.t.callExpression(value, [elNode]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return el
|
|
||||||
? this.t.expressionStatement(
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier('View'), this.t.identifier('addDidMount')), [
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.arrowFunctionExpression([], elementNode),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
: this.t.expressionStatement(elementNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Utils
|
|
||||||
private isOnlyMemberExpression(value: t.Expression): boolean {
|
|
||||||
if (!this.t.isMemberExpression(value)) return false;
|
|
||||||
while (value.property) {
|
|
||||||
if (this.t.isMemberExpression(value.property)) {
|
|
||||||
value = value.property;
|
|
||||||
continue;
|
|
||||||
} else if (this.t.isIdentifier(value.property)) break;
|
|
||||||
else return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import ElementGenerator from './ElementGenerator';
|
|
||||||
|
|
||||||
export default class ForwardPropsGenerator extends ElementGenerator {
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* this._$forwardProp(${dlNodeName})
|
|
||||||
*/
|
|
||||||
forwardProps(dlNodeName: string): t.ExpressionStatement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.thisExpression(), this.t.identifier('_$addForwardProps')), [
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,363 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { DLError } from '../error';
|
|
||||||
import ForwardPropGenerator from './ForwardPropGenerator';
|
|
||||||
|
|
||||||
export default class HTMLPropGenerator extends ForwardPropGenerator {
|
|
||||||
static DelegatedEvents = new Set([
|
|
||||||
'beforeinput',
|
|
||||||
'click',
|
|
||||||
'dblclick',
|
|
||||||
'contextmenu',
|
|
||||||
'focusin',
|
|
||||||
'focusout',
|
|
||||||
'input',
|
|
||||||
'keydown',
|
|
||||||
'keyup',
|
|
||||||
'mousedown',
|
|
||||||
'mousemove',
|
|
||||||
'mouseout',
|
|
||||||
'mouseover',
|
|
||||||
'mouseup',
|
|
||||||
'pointerdown',
|
|
||||||
'pointermove',
|
|
||||||
'pointerout',
|
|
||||||
'pointerover',
|
|
||||||
'pointerup',
|
|
||||||
'touchend',
|
|
||||||
'touchmove',
|
|
||||||
'touchstart',
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add any HTML props according to the key
|
|
||||||
* @param name
|
|
||||||
* @param tag
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
* @param dependencyIndexArr
|
|
||||||
* @returns t.Statement
|
|
||||||
*/
|
|
||||||
addHTMLProp(
|
|
||||||
name: string,
|
|
||||||
tag: string,
|
|
||||||
key: string,
|
|
||||||
value: t.Expression,
|
|
||||||
dynamic: boolean,
|
|
||||||
dependencyIndexArr: number[],
|
|
||||||
dependenciesNode: t.ArrayExpression
|
|
||||||
): t.Statement | null {
|
|
||||||
// ---- Dynamic HTML prop with init and update
|
|
||||||
if (dynamic) {
|
|
||||||
this.addUpdateStatements(
|
|
||||||
dependencyIndexArr,
|
|
||||||
this.setDynamicHTMLProp(name, tag, key, value, dependenciesNode, true)
|
|
||||||
);
|
|
||||||
return this.setDynamicHTMLProp(name, tag, key, value, dependenciesNode, false);
|
|
||||||
}
|
|
||||||
// ---- Static HTML prop with init only
|
|
||||||
return this.setStaticHTMLProp(name, tag, key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* insertNode(${dlNodeName}, ${childNodeName}, ${position})
|
|
||||||
*/
|
|
||||||
insertNode(dlNodeName: string, childNodeName: string, position: number): t.ExpressionStatement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.insertNode), [
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.identifier(childNodeName),
|
|
||||||
this.t.numericLiteral(position),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} && ${expression}
|
|
||||||
*/
|
|
||||||
private setPropWithCheck(dlNodeName: string, expression: t.Expression, check: boolean): t.Statement {
|
|
||||||
if (check) {
|
|
||||||
return this.optionalExpression(dlNodeName, expression);
|
|
||||||
}
|
|
||||||
return this.t.expressionStatement(expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* setStyle(${dlNodeName}, ${value})
|
|
||||||
*/
|
|
||||||
private setHTMLStyle(dlNodeName: string, value: t.Expression, check: boolean): t.Statement {
|
|
||||||
return this.setPropWithCheck(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.setStyle), [this.t.identifier(dlNodeName), value]),
|
|
||||||
check
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* setStyle(${dlNodeName}, ${value})
|
|
||||||
*/
|
|
||||||
private setHTMLDataset(dlNodeName: string, value: t.Expression, check: boolean): t.Statement {
|
|
||||||
return this.setPropWithCheck(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.setDataset), [this.t.identifier(dlNodeName), value]),
|
|
||||||
check
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}.${key} = ${value}
|
|
||||||
*/
|
|
||||||
private setHTMLProp(dlNodeName: string, key: string, value: t.Expression): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier(key)),
|
|
||||||
value
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}.setAttribute(${key}, ${value})
|
|
||||||
*/
|
|
||||||
private setHTMLAttr(dlNodeName: string, key: string, value: t.Expression): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('setAttribute')), [
|
|
||||||
this.t.stringLiteral(key),
|
|
||||||
value,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}.addEventListener(${key}, ${value})
|
|
||||||
*/
|
|
||||||
private setHTMLEvent(dlNodeName: string, key: string, value: t.Expression): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.callExpression(
|
|
||||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('addEventListener')),
|
|
||||||
[this.t.stringLiteral(key), value]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* setEvent(${dlNodeName}, ${key}, ${value})
|
|
||||||
*/
|
|
||||||
private setEvent(dlNodeName: string, key: string, value: t.Expression, check: boolean): t.Statement {
|
|
||||||
return this.setPropWithCheck(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.setEvent), [
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.stringLiteral(key),
|
|
||||||
value,
|
|
||||||
]),
|
|
||||||
check
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* delegateEvent(${dlNodeName}, ${key}, ${value})
|
|
||||||
*/
|
|
||||||
private delegateEvent(dlNodeName: string, key: string, value: t.Expression, check: boolean): t.Statement {
|
|
||||||
return this.setPropWithCheck(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.delegateEvent), [
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.stringLiteral(key),
|
|
||||||
value,
|
|
||||||
]),
|
|
||||||
check
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* setHTMLProp(${dlNodeName}, ${key}, ${valueFunc}, ${dependenciesNode})
|
|
||||||
*/
|
|
||||||
private setCachedProp(
|
|
||||||
dlNodeName: string,
|
|
||||||
key: string,
|
|
||||||
value: t.Expression,
|
|
||||||
dependenciesNode: t.ArrayExpression,
|
|
||||||
check: boolean
|
|
||||||
): t.Statement {
|
|
||||||
return this.setPropWithCheck(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.setHTMLProp), [
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.stringLiteral(key),
|
|
||||||
this.t.arrowFunctionExpression([], value),
|
|
||||||
dependenciesNode,
|
|
||||||
]),
|
|
||||||
check
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* setHTMLAttr(${dlNodeName}, ${key}, ${valueFunc}, ${dependenciesNode}, ${check})
|
|
||||||
*/
|
|
||||||
private setCachedAttr(
|
|
||||||
dlNodeName: string,
|
|
||||||
key: string,
|
|
||||||
value: t.Expression,
|
|
||||||
dependenciesNode: t.ArrayExpression,
|
|
||||||
check: boolean
|
|
||||||
): t.Statement {
|
|
||||||
return this.setPropWithCheck(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.setHTMLAttr), [
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.stringLiteral(key),
|
|
||||||
this.t.arrowFunctionExpression([], value),
|
|
||||||
dependenciesNode,
|
|
||||||
]),
|
|
||||||
check
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* setHTMLProps(${dlNodeName}, ${value})
|
|
||||||
*/
|
|
||||||
private setHTMLPropObject(dlNodeName: string, value: t.Expression, check: boolean): t.Statement {
|
|
||||||
return this.setPropWithCheck(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.setHTMLProps), [this.t.identifier(dlNodeName), value]),
|
|
||||||
check
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* setHTMLAttrs(${dlNodeName}, ${value})
|
|
||||||
*/
|
|
||||||
private setHTMLAttrObject(dlNodeName: string, value: t.Expression, check: boolean): t.Statement {
|
|
||||||
return this.setPropWithCheck(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.setHTMLAttrs), [this.t.identifier(dlNodeName), value]),
|
|
||||||
check
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static commonHTMLPropKeys = [
|
|
||||||
'style',
|
|
||||||
'dataset',
|
|
||||||
'props',
|
|
||||||
'ref',
|
|
||||||
'attrs',
|
|
||||||
'forwardProps',
|
|
||||||
...HTMLPropGenerator.lifecycle,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For style/dataset/ref/attr/prop
|
|
||||||
*/
|
|
||||||
private addCommonHTMLProp(
|
|
||||||
dlNodeName: string,
|
|
||||||
attrName: string,
|
|
||||||
value: t.Expression,
|
|
||||||
check: boolean
|
|
||||||
): t.Statement | null {
|
|
||||||
if (HTMLPropGenerator.lifecycle.includes(attrName as (typeof HTMLPropGenerator.lifecycle)[number])) {
|
|
||||||
if (!check) return this.addLifecycle(dlNodeName, attrName as (typeof HTMLPropGenerator.lifecycle)[number], value);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (attrName === 'ref') {
|
|
||||||
if (!check) return this.initElement(dlNodeName, value);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (attrName === 'style') return this.setHTMLStyle(dlNodeName, value, check);
|
|
||||||
if (attrName === 'dataset') return this.setHTMLDataset(dlNodeName, value, check);
|
|
||||||
if (attrName === 'props') return this.setHTMLPropObject(dlNodeName, value, check);
|
|
||||||
if (attrName === 'attrs') return this.setHTMLAttrObject(dlNodeName, value, check);
|
|
||||||
if (attrName === 'forwardProps') return this.forwardProps(dlNodeName);
|
|
||||||
return DLError.throw2();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* 1. Event listener
|
|
||||||
* - ${dlNodeName}.addEventListener(${key}, ${value})
|
|
||||||
* 2. HTML internal attribute -> DOM property
|
|
||||||
* - ${dlNodeName}.${key} = ${value}
|
|
||||||
* 3. HTML custom attribute
|
|
||||||
* - ${dlNodeName}.setAttribute(${key}, ${value})
|
|
||||||
*/
|
|
||||||
private setStaticHTMLProp(
|
|
||||||
dlNodeName: string,
|
|
||||||
tag: string,
|
|
||||||
attrName: string,
|
|
||||||
value: t.Expression
|
|
||||||
): t.Statement | null {
|
|
||||||
if (HTMLPropGenerator.commonHTMLPropKeys.includes(attrName))
|
|
||||||
return this.addCommonHTMLProp(dlNodeName, attrName, value, false);
|
|
||||||
if (attrName.startsWith('on')) {
|
|
||||||
const eventName = attrName.slice(2).toLowerCase();
|
|
||||||
if (HTMLPropGenerator.DelegatedEvents.has(eventName)) {
|
|
||||||
return this.delegateEvent(dlNodeName, eventName, value, false);
|
|
||||||
}
|
|
||||||
return this.setHTMLEvent(dlNodeName, eventName, value);
|
|
||||||
}
|
|
||||||
if (this.isInternalAttribute(tag, attrName)) {
|
|
||||||
if (attrName === 'class') attrName = 'className';
|
|
||||||
else if (attrName === 'for') attrName = 'htmlFor';
|
|
||||||
return this.setHTMLProp(dlNodeName, attrName, value);
|
|
||||||
}
|
|
||||||
return this.setHTMLAttr(dlNodeName, attrName, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* 1. Event listener
|
|
||||||
* - ${setEvent}(${dlNodeName}, ${key}, ${value})
|
|
||||||
* 2. HTML internal attribute -> DOM property
|
|
||||||
* - ${setHTMLProp}(${dlNodeName}, ${key}, ${value})
|
|
||||||
* 3. HTML custom attribute
|
|
||||||
* - ${setHTMLAttr}(${dlNodeName}, ${key}, ${value})
|
|
||||||
*/
|
|
||||||
private setDynamicHTMLProp(
|
|
||||||
dlNodeName: string,
|
|
||||||
tag: string,
|
|
||||||
attrName: string,
|
|
||||||
value: t.Expression,
|
|
||||||
dependenciesNode: t.ArrayExpression,
|
|
||||||
check: boolean
|
|
||||||
): t.Statement | null {
|
|
||||||
if (HTMLPropGenerator.commonHTMLPropKeys.includes(attrName))
|
|
||||||
return this.addCommonHTMLProp(dlNodeName, attrName, value, check);
|
|
||||||
if (attrName.startsWith('on')) {
|
|
||||||
const eventName = attrName.slice(2).toLowerCase();
|
|
||||||
if (HTMLPropGenerator.DelegatedEvents.has(eventName)) {
|
|
||||||
return this.delegateEvent(dlNodeName, eventName, value, check);
|
|
||||||
}
|
|
||||||
return this.setEvent(dlNodeName, eventName, value, check);
|
|
||||||
}
|
|
||||||
if (this.alterAttributeMap[attrName]) {
|
|
||||||
attrName = this.alterAttributeMap[attrName];
|
|
||||||
}
|
|
||||||
if (this.isInternalAttribute(tag, attrName)) {
|
|
||||||
return this.setCachedProp(dlNodeName, attrName, value, dependenciesNode, check);
|
|
||||||
}
|
|
||||||
return this.setCachedAttr(dlNodeName, attrName, value, dependenciesNode, check);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if the attribute is internal, i.e., can be accessed as js property
|
|
||||||
* @param tag
|
|
||||||
* @param attribute
|
|
||||||
* @returns true if the attribute is internal
|
|
||||||
*/
|
|
||||||
isInternalAttribute(tag: string, attribute: string): boolean {
|
|
||||||
return this.elementAttributeMap['*']?.includes(attribute) || this.elementAttributeMap[tag]?.includes(attribute);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import BaseGenerator from './BaseGenerator';
|
|
||||||
|
|
||||||
export default class LifecycleGenerator extends BaseGenerator {
|
|
||||||
static lifecycle = ['willMount', 'didMount', 'willUnmount', 'didUnmount'] as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} && ${value}(${dlNodeName}, changed)
|
|
||||||
*/
|
|
||||||
addOnUpdate(dlNodeName: string, value: t.Expression): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.logicalExpression(
|
|
||||||
'&&',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.callExpression(value, [this.t.identifier(dlNodeName), ...this.updateParams.slice(1)])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* willMount:
|
|
||||||
* - ${value}(${dlNodeName})
|
|
||||||
* didMount/willUnmount/didUnmount:
|
|
||||||
* - View.addDidMount(${dlNodeName}, ${value})
|
|
||||||
*/
|
|
||||||
addLifecycle(
|
|
||||||
dlNodeName: string,
|
|
||||||
key: (typeof LifecycleGenerator.lifecycle)[number],
|
|
||||||
value: t.Expression
|
|
||||||
): t.Statement {
|
|
||||||
if (key === 'willMount') {
|
|
||||||
return this.addWillMount(dlNodeName, value);
|
|
||||||
}
|
|
||||||
return this.addOtherLifecycle(dlNodeName, value, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${value}(${dlNodeName})
|
|
||||||
*/
|
|
||||||
addWillMount(dlNodeName: string, value: t.Expression): t.ExpressionStatement {
|
|
||||||
return this.t.expressionStatement(this.t.callExpression(value, [this.t.identifier(dlNodeName)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* View.addDidMount(${dlNodeName}, ${value})
|
|
||||||
*/
|
|
||||||
addOtherLifecycle(
|
|
||||||
dlNodeName: string,
|
|
||||||
value: t.Expression,
|
|
||||||
type: 'didMount' | 'willUnmount' | 'didUnmount'
|
|
||||||
): t.ExpressionStatement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.callExpression(
|
|
||||||
this.t.memberExpression(
|
|
||||||
this.t.identifier('View'),
|
|
||||||
this.t.identifier(`add${type[0].toUpperCase()}${type.slice(1)}`)
|
|
||||||
),
|
|
||||||
[this.t.identifier(dlNodeName), value]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { type DependencyProp, type ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
import LifecycleGenerator from './LifecycleGenerator';
|
|
||||||
|
|
||||||
export default class PropViewGenerator extends LifecycleGenerator {
|
|
||||||
/**
|
|
||||||
* @brief Alter prop view in batch
|
|
||||||
* @param props
|
|
||||||
* @returns altered props
|
|
||||||
*/
|
|
||||||
alterPropViews<T extends Record<string, DependencyProp> | undefined>(props: T): T {
|
|
||||||
if (!props) return props;
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(props).map(([key, prop]) => {
|
|
||||||
return [key, this.alterPropView(prop)!];
|
|
||||||
})
|
|
||||||
) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} = new PropView(($addUpdate) => {
|
|
||||||
* addUpdate((changed) => { ${updateStatements} })
|
|
||||||
* ${initStatements}
|
|
||||||
* return ${topLevelNodes}
|
|
||||||
* })
|
|
||||||
*/
|
|
||||||
declarePropView(viewParticles: ViewParticle[]) {
|
|
||||||
// ---- Generate PropView
|
|
||||||
const [initStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren(
|
|
||||||
viewParticles,
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
// ---- Add update function to the first node
|
|
||||||
/**
|
|
||||||
* $addUpdate((changed) => { ${updateStatements} })
|
|
||||||
*/
|
|
||||||
if (Object.keys(updateStatements).length > 0) {
|
|
||||||
initStatements.unshift(
|
|
||||||
this.t.expressionStatement(
|
|
||||||
this.t.callExpression(this.t.identifier('$addUpdate'), [
|
|
||||||
this.t.arrowFunctionExpression(this.updateParams, this.geneUpdateBody(updateStatements)),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
initStatements.unshift(...this.declareNodes(nodeIdx));
|
|
||||||
initStatements.push(this.generateReturnStatement(topLevelNodes));
|
|
||||||
|
|
||||||
// ---- Assign as a dlNode
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
const propViewNode = this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.newExpression(this.t.identifier(this.importMap.PropView), [
|
|
||||||
this.t.arrowFunctionExpression([this.t.identifier('$addUpdate')], this.t.blockStatement(initStatements)),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.addInitStatement(propViewNode);
|
|
||||||
const propViewIdentifier = this.t.identifier(dlNodeName);
|
|
||||||
|
|
||||||
// ---- Add to update statements
|
|
||||||
/**
|
|
||||||
* ${dlNodeName}?.update(changed)
|
|
||||||
*/
|
|
||||||
this.addUpdateStatementsWithoutDep(
|
|
||||||
this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(
|
|
||||||
this.t.memberExpression(propViewIdentifier, this.t.identifier('update')),
|
|
||||||
this.updateParams
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Alter prop view by replacing prop view with a recursively generated prop view
|
|
||||||
* @param prop
|
|
||||||
* @returns altered prop
|
|
||||||
*/
|
|
||||||
alterPropView<T extends DependencyProp | undefined>(prop: T): T {
|
|
||||||
if (!prop) return prop;
|
|
||||||
const { value, viewPropMap } = prop;
|
|
||||||
if (!viewPropMap) return { ...prop, value };
|
|
||||||
let newValue = value;
|
|
||||||
this.traverse(this.valueWrapper(value), {
|
|
||||||
StringLiteral: innerPath => {
|
|
||||||
const id = innerPath.node.value;
|
|
||||||
const viewParticles = viewPropMap[id];
|
|
||||||
if (!viewParticles) return;
|
|
||||||
const propViewIdentifier = this.t.identifier(this.declarePropView(viewParticles));
|
|
||||||
|
|
||||||
if (value === innerPath.node) newValue = propViewIdentifier;
|
|
||||||
innerPath.replaceWith(propViewIdentifier);
|
|
||||||
innerPath.skip();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return { ...prop, value: newValue };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the dependency index array from the update statements' keys
|
|
||||||
* i.e. [1, 2, 7] => [0b1, 0b10, 0b111] => [[1], [2], [0, 1, 2]] => [0, 1, 2]
|
|
||||||
* @param updateStatements
|
|
||||||
* @returns dependency index array
|
|
||||||
*/
|
|
||||||
private static reverseDependencyIndexArr(updateStatements: Record<number, t.Statement[]>): number[] {
|
|
||||||
const allDepsNum = Object.keys(updateStatements)
|
|
||||||
.map(Number)
|
|
||||||
.reduce((acc, depNum) => acc | depNum, 0);
|
|
||||||
const allDeps = [];
|
|
||||||
for (let i = 0; i < String(allDepsNum).length; i++) {
|
|
||||||
if (allDepsNum & (1 << i)) allDeps.push(i);
|
|
||||||
}
|
|
||||||
return allDeps;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { type ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
import ViewGenerator from './ViewGenerator';
|
|
||||||
|
|
||||||
export default class MainViewGenerator extends ViewGenerator {
|
|
||||||
/**
|
|
||||||
* @brief Generate the main view, i.e., View() { ... }
|
|
||||||
* @param viewParticles
|
|
||||||
* @returns [viewBody, classProperties, templateIdx]
|
|
||||||
*/
|
|
||||||
generate(viewParticles: ViewParticle[]): [t.BlockStatement, t.ClassProperty[], number] {
|
|
||||||
const allClassProperties: t.ClassProperty[] = [];
|
|
||||||
const allInitStatements: t.Statement[] = [];
|
|
||||||
const allUpdateStatements: Record<number, t.Statement[]> = {};
|
|
||||||
const topLevelNodes: string[] = [];
|
|
||||||
|
|
||||||
viewParticles.forEach(viewParticle => {
|
|
||||||
const [initStatements, updateStatements, classProperties, nodeName] = this.generateChild(viewParticle);
|
|
||||||
allInitStatements.push(...initStatements);
|
|
||||||
Object.entries(updateStatements).forEach(([depNum, statements]) => {
|
|
||||||
if (!allUpdateStatements[Number(depNum)]) {
|
|
||||||
allUpdateStatements[Number(depNum)] = [];
|
|
||||||
}
|
|
||||||
allUpdateStatements[Number(depNum)].push(...statements);
|
|
||||||
});
|
|
||||||
allClassProperties.push(...classProperties);
|
|
||||||
topLevelNodes.push(nodeName);
|
|
||||||
});
|
|
||||||
|
|
||||||
const viewBody = this.t.blockStatement([
|
|
||||||
...this.declareNodes(),
|
|
||||||
...this.geneUpdate(allUpdateStatements),
|
|
||||||
...allInitStatements,
|
|
||||||
this.geneReturn(topLevelNodes),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [viewBody, allClassProperties, this.templateIdx];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* this._$update = ($changed) => {
|
|
||||||
* if ($changed & 1) {
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
private geneUpdate(updateStatements: Record<number, t.Statement[]>): t.Statement[] {
|
|
||||||
if (Object.keys(updateStatements).length === 0) return [];
|
|
||||||
return [
|
|
||||||
this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.memberExpression(this.t.thisExpression(), this.t.identifier('_$update'), false),
|
|
||||||
this.t.arrowFunctionExpression(
|
|
||||||
this.updateParams,
|
|
||||||
this.t.blockStatement([
|
|
||||||
...Object.entries(updateStatements)
|
|
||||||
.filter(([depNum]) => depNum !== '0')
|
|
||||||
.map(([depNum, statements]) => {
|
|
||||||
return this.t.ifStatement(
|
|
||||||
this.t.binaryExpression('&', this.t.identifier('$changed'), this.t.numericLiteral(Number(depNum))),
|
|
||||||
this.t.blockStatement(statements)
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
...(updateStatements[0] ?? []),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* return [${nodeNames}]
|
|
||||||
*/
|
|
||||||
private geneReturn(topLevelNodes: string[]) {
|
|
||||||
return this.t.returnStatement(this.t.arrayExpression(topLevelNodes.map(nodeName => this.t.identifier(nodeName))));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,153 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { type DependencyProp, type CompParticle, type ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
import ForwardPropGenerator from '../HelperGenerators/ForwardPropGenerator';
|
|
||||||
|
|
||||||
export default class CompGenerator extends ForwardPropGenerator {
|
|
||||||
run() {
|
|
||||||
let { props } = this.viewParticle as CompParticle;
|
|
||||||
props = this.alterPropViews(props);
|
|
||||||
const { tag, children } = this.viewParticle as CompParticle;
|
|
||||||
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
|
|
||||||
this.addInitStatement(...this.declareCompNode(dlNodeName, tag, props, children));
|
|
||||||
const allDependencyIndexArr: number[] = [];
|
|
||||||
|
|
||||||
// ---- Resolve props
|
|
||||||
Object.entries(props).forEach(([key, { value, dependencyIndexArr, dependenciesNode }]) => {
|
|
||||||
if (key === 'forwardProps') return;
|
|
||||||
if (key === 'didUpdate') return;
|
|
||||||
allDependencyIndexArr.push(...dependencyIndexArr);
|
|
||||||
if (CompGenerator.lifecycle.includes(key as (typeof CompGenerator.lifecycle)[number])) {
|
|
||||||
this.addInitStatement(this.addLifecycle(dlNodeName, key as (typeof CompGenerator.lifecycle)[number], value));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key === 'ref') {
|
|
||||||
this.addInitStatement(this.initElement(dlNodeName, value));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key === 'elements') {
|
|
||||||
this.addInitStatement(this.initElement(dlNodeName, value, true));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key === '_$content') {
|
|
||||||
this.addUpdateStatements(dependencyIndexArr, this.setCompContent(dlNodeName, value, dependenciesNode));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key === 'props') {
|
|
||||||
this.addUpdateStatements(dependencyIndexArr, this.setCompProps(dlNodeName, value, dependenciesNode));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addUpdateStatements(dependencyIndexArr, this.setCompProp(dlNodeName, key, value, dependenciesNode));
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- Add addUpdate last
|
|
||||||
if (props.didUpdate) {
|
|
||||||
this.addUpdateStatements(allDependencyIndexArr, this.addOnUpdate(dlNodeName, props.didUpdate.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* null
|
|
||||||
* or
|
|
||||||
* [[prop1, value1, deps1], [prop2, value2, deps2], ...
|
|
||||||
*/
|
|
||||||
private generateCompProps(props: Record<string, DependencyProp>): t.Expression {
|
|
||||||
if (Object.keys(props).length === 0) return this.t.nullLiteral();
|
|
||||||
return this.t.arrayExpression(
|
|
||||||
Object.entries(props).map(([key, { value, dependenciesNode }]) => {
|
|
||||||
return this.t.arrayExpression([this.t.stringLiteral(key), value, dependenciesNode]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} = new ${tag}()
|
|
||||||
* ${dlNodeName}._$init(${props}, ${content}, ${children}, ${this})
|
|
||||||
*/
|
|
||||||
private declareCompNode(
|
|
||||||
dlNodeName: string,
|
|
||||||
tag: t.Expression,
|
|
||||||
props: Record<string, DependencyProp>,
|
|
||||||
children: ViewParticle[]
|
|
||||||
): t.Statement[] {
|
|
||||||
const willForwardProps = 'forwardProps' in props;
|
|
||||||
const newProps = Object.fromEntries(
|
|
||||||
Object.entries(props).filter(
|
|
||||||
([key]) =>
|
|
||||||
!['ref', 'elements', 'forwardProps', '_$content', 'didUpdate', 'props', ...CompGenerator.lifecycle].includes(
|
|
||||||
key
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = props._$content;
|
|
||||||
|
|
||||||
return [
|
|
||||||
this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression('=', this.t.identifier(dlNodeName), this.t.newExpression(tag, []))
|
|
||||||
),
|
|
||||||
this.t.expressionStatement(
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$init')), [
|
|
||||||
this.generateCompProps(newProps),
|
|
||||||
content ? this.t.arrayExpression([content.value, content.dependenciesNode]) : this.t.nullLiteral(),
|
|
||||||
children.length > 0 ? this.t.identifier(this.declarePropView(children)) : this.t.nullLiteral(),
|
|
||||||
willForwardProps ? this.t.identifier('this') : this.t.nullLiteral(),
|
|
||||||
])
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}._$setContent(() => ${value}, ${dependenciesNode})
|
|
||||||
*/
|
|
||||||
private setCompContent(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$setContent')), [
|
|
||||||
this.t.arrowFunctionExpression([], value),
|
|
||||||
dependenciesNode,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}._$setProp(${key}, () => ${value}, ${dependenciesNode})
|
|
||||||
*/
|
|
||||||
private setCompProp(
|
|
||||||
dlNodeName: string,
|
|
||||||
key: string,
|
|
||||||
value: t.Expression,
|
|
||||||
dependenciesNode: t.ArrayExpression
|
|
||||||
): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$setProp')), [
|
|
||||||
this.t.stringLiteral(key),
|
|
||||||
this.t.arrowFunctionExpression([], value),
|
|
||||||
dependenciesNode,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}._$setProps(() => ${value}, ${dependenciesNode})
|
|
||||||
*/
|
|
||||||
private setCompProps(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$setProps')), [
|
|
||||||
this.t.arrowFunctionExpression([], value),
|
|
||||||
dependenciesNode,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { type ViewParticle, type DependencyProp, type EnvParticle } from '@openinula/reactivity-parser';
|
|
||||||
import PropViewGenerator from '../HelperGenerators/PropViewGenerator';
|
|
||||||
|
|
||||||
export default class EnvGenerator extends PropViewGenerator {
|
|
||||||
run() {
|
|
||||||
let { props } = this.viewParticle as EnvParticle;
|
|
||||||
props = this.alterPropViews(props)!;
|
|
||||||
const { children } = this.viewParticle as EnvParticle;
|
|
||||||
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
|
|
||||||
this.addInitStatement(this.declareEnvNode(dlNodeName, props));
|
|
||||||
|
|
||||||
// ---- Children
|
|
||||||
this.addInitStatement(this.geneEnvChildren(dlNodeName, children));
|
|
||||||
|
|
||||||
// ---- Update props
|
|
||||||
Object.entries(props).forEach(([key, { dependencyIndexArr, value, dependenciesNode }]) => {
|
|
||||||
if (!dependencyIndexArr) return;
|
|
||||||
this.addUpdateStatements(dependencyIndexArr, this.updateEnvNode(dlNodeName, key, value, dependenciesNode));
|
|
||||||
});
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* { ${key}: ${value}, ... }
|
|
||||||
* { ${key}: ${deps}, ... }
|
|
||||||
*/
|
|
||||||
private generateEnvs(props: Record<string, DependencyProp>): t.Expression[] {
|
|
||||||
return [
|
|
||||||
this.t.objectExpression(
|
|
||||||
Object.entries(props).map(([key, { value }]) => this.t.objectProperty(this.t.identifier(key), value))
|
|
||||||
),
|
|
||||||
this.t.objectExpression(
|
|
||||||
Object.entries(props)
|
|
||||||
.map(
|
|
||||||
([key, { dependenciesNode }]) =>
|
|
||||||
dependenciesNode && this.t.objectProperty(this.t.identifier(key), dependenciesNode)
|
|
||||||
)
|
|
||||||
.filter(Boolean) as t.ObjectProperty[]
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} = new EnvNode(envs)
|
|
||||||
*/
|
|
||||||
private declareEnvNode(dlNodeName: string, props: Record<string, DependencyProp>): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.newExpression(this.t.identifier(this.importMap.EnvNode), this.generateEnvs(props))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}.initNodes([${childrenNames}])
|
|
||||||
*/
|
|
||||||
private geneEnvChildren(dlNodeName: string, children: ViewParticle[]): t.Statement {
|
|
||||||
const [statements, childrenNames] = this.generateChildren(children);
|
|
||||||
this.addInitStatement(...statements);
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('initNodes')), [
|
|
||||||
this.t.arrayExpression(childrenNames.map(name => this.t.identifier(name))),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}.updateEnv(${key}, () => ${value}, ${dependenciesNode})
|
|
||||||
*/
|
|
||||||
private updateEnvNode(
|
|
||||||
dlNodeName: string,
|
|
||||||
key: string,
|
|
||||||
value: t.Expression,
|
|
||||||
dependenciesNode: t.ArrayExpression
|
|
||||||
): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('updateEnv')), [
|
|
||||||
this.t.stringLiteral(key),
|
|
||||||
this.t.arrowFunctionExpression([], value),
|
|
||||||
dependenciesNode,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { type ExpParticle } from '@openinula/reactivity-parser';
|
|
||||||
import ElementGenerator from '../HelperGenerators/ElementGenerator';
|
|
||||||
import { DLError } from '../error';
|
|
||||||
|
|
||||||
export default class ExpGenerator extends ElementGenerator {
|
|
||||||
run() {
|
|
||||||
let { content, props } = this.viewParticle as ExpParticle;
|
|
||||||
content = this.alterPropView(content)!;
|
|
||||||
props = this.alterPropViews(props);
|
|
||||||
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
|
|
||||||
this.addInitStatement(this.declareExpNode(dlNodeName, content.value, content.dependenciesNode));
|
|
||||||
|
|
||||||
if (content.dynamic) {
|
|
||||||
this.addUpdateStatements(
|
|
||||||
content.dependencyIndexArr,
|
|
||||||
this.updateExpNode(dlNodeName, content.value, content.dependenciesNode)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props) {
|
|
||||||
Object.entries(props).forEach(([key, { value }]) => {
|
|
||||||
if (ExpGenerator.lifecycle.includes(key as (typeof ExpGenerator.lifecycle)[number])) {
|
|
||||||
return this.addInitStatement(
|
|
||||||
this.addLifecycle(dlNodeName, key as (typeof ExpGenerator.lifecycle)[number], value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (key === 'ref') {
|
|
||||||
return this.addInitStatement(this.initElement(dlNodeName, value));
|
|
||||||
}
|
|
||||||
if (key === 'elements') {
|
|
||||||
return this.addInitStatement(this.initElement(dlNodeName, value, true));
|
|
||||||
}
|
|
||||||
if (key === 'didUpdate') {
|
|
||||||
return this.addUpdateStatements(content.dependencyIndexArr, this.addOnUpdate(dlNodeName, value));
|
|
||||||
}
|
|
||||||
DLError.warn1(key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} = new ExpNode(${value}, dependenciesNode)
|
|
||||||
*/
|
|
||||||
private declareExpNode(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.newExpression(this.t.identifier(this.importMap.ExpNode), [
|
|
||||||
value,
|
|
||||||
dependenciesNode ?? this.t.nullLiteral(),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}.update(() => value, dependenciesNode)
|
|
||||||
*/
|
|
||||||
private updateExpNode(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('update')), [
|
|
||||||
this.t.arrowFunctionExpression([], value),
|
|
||||||
dependenciesNode ?? this.t.nullLiteral(),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import BaseGenerator from '../HelperGenerators/BaseGenerator';
|
|
||||||
import { type ForParticle, type ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
|
|
||||||
export default class ForGenerator extends BaseGenerator {
|
|
||||||
run() {
|
|
||||||
const { item, array, key, children } = this.viewParticle as ForParticle;
|
|
||||||
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
|
|
||||||
// ---- Declare for node
|
|
||||||
this.addInitStatement(
|
|
||||||
this.declareForNode(
|
|
||||||
dlNodeName,
|
|
||||||
array.value,
|
|
||||||
item,
|
|
||||||
children,
|
|
||||||
BaseGenerator.calcDependencyNum(array.dependencyIndexArr),
|
|
||||||
key
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// ---- Update statements
|
|
||||||
this.addUpdateStatements(array.dependencyIndexArr, this.updateForNode(dlNodeName, array.value, item, key));
|
|
||||||
this.addUpdateStatementsWithoutDep(this.updateForNodeItem(dlNodeName));
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} = new ForNode(${array}, ${depNum}, ${array}.map(${item} => ${key}),
|
|
||||||
* ((${item}, $updateArr, $idx) => {
|
|
||||||
* $updateArr[$idx] = (changed, $item) => {
|
|
||||||
* ${item} = $item
|
|
||||||
* {$updateStatements}
|
|
||||||
* })
|
|
||||||
* ${children}
|
|
||||||
* return [...${topLevelNodes}]
|
|
||||||
* })
|
|
||||||
*/
|
|
||||||
private declareForNode(
|
|
||||||
dlNodeName: string,
|
|
||||||
array: t.Expression,
|
|
||||||
item: t.LVal,
|
|
||||||
children: ViewParticle[],
|
|
||||||
depNum: number,
|
|
||||||
key: t.Expression
|
|
||||||
): t.Statement {
|
|
||||||
// ---- NodeFunc
|
|
||||||
const [childStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren(children, false, true);
|
|
||||||
|
|
||||||
// ---- Update func
|
|
||||||
childStatements.unshift(
|
|
||||||
...this.declareNodes(nodeIdx),
|
|
||||||
this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.memberExpression(this.t.identifier('$updateArr'), this.t.identifier('$idx'), true),
|
|
||||||
this.t.arrowFunctionExpression(
|
|
||||||
[...this.updateParams, this.t.identifier('$item')],
|
|
||||||
this.t.blockStatement([
|
|
||||||
this.t.expressionStatement(this.t.assignmentExpression('=', item, this.t.identifier('$item'))),
|
|
||||||
...this.geneUpdateBody(updateStatements).body,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// ---- Return statement
|
|
||||||
childStatements.push(this.generateReturnStatement(topLevelNodes));
|
|
||||||
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.newExpression(this.t.identifier(this.importMap.ForNode), [
|
|
||||||
array,
|
|
||||||
this.t.numericLiteral(depNum),
|
|
||||||
this.getForKeyStatement(array, item, key),
|
|
||||||
this.t.arrowFunctionExpression(
|
|
||||||
[item as any, this.t.identifier('$updateArr'), this.t.identifier('$idx')],
|
|
||||||
this.t.blockStatement(childStatements)
|
|
||||||
),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${array}.map(${item} => ${key})
|
|
||||||
*/
|
|
||||||
private getForKeyStatement(array: t.Expression, item: t.LVal, key: t.Expression): t.Expression {
|
|
||||||
return this.t.isNullLiteral(key)
|
|
||||||
? key
|
|
||||||
: this.t.callExpression(this.t.memberExpression(array, this.t.identifier('map')), [
|
|
||||||
this.t.arrowFunctionExpression([item as any], key),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}.updateArray(${array}, ${array}.map(${item} => ${key}))
|
|
||||||
*/
|
|
||||||
private updateForNode(dlNodeName: string, array: t.Expression, item: t.LVal, key: t.Expression): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('updateArray')), [
|
|
||||||
array,
|
|
||||||
...this.updateParams.slice(1),
|
|
||||||
this.getForKeyStatement(array, item, key),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}?.update(changed)
|
|
||||||
*/
|
|
||||||
private updateForNodeItem(dlNodeName: string): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(
|
|
||||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('update')),
|
|
||||||
this.updateParams
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { type HTMLParticle } from '@openinula/reactivity-parser';
|
|
||||||
import HTMLPropGenerator from '../HelperGenerators/HTMLPropGenerator';
|
|
||||||
|
|
||||||
export default class HTMLGenerator extends HTMLPropGenerator {
|
|
||||||
run() {
|
|
||||||
const { tag, props, children } = this.viewParticle as HTMLParticle;
|
|
||||||
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
|
|
||||||
this.addInitStatement(this.declareHTMLNode(dlNodeName, tag));
|
|
||||||
|
|
||||||
// ---- Resolve props
|
|
||||||
// ---- Use the tag name to check if the prop is internal for the tag,
|
|
||||||
// for dynamic tag, we can't check it, so we just assume it's not internal
|
|
||||||
// represent by the "ANY" tag name
|
|
||||||
const tagName = this.t.isStringLiteral(tag) ? tag.value : 'ANY';
|
|
||||||
const allDependencyIndexArr: number[] = [];
|
|
||||||
Object.entries(props).forEach(([key, { value, dependencyIndexArr, dependenciesNode, dynamic }]) => {
|
|
||||||
if (key === 'didUpdate') return;
|
|
||||||
allDependencyIndexArr.push(...(dependencyIndexArr ?? []));
|
|
||||||
this.addInitStatement(
|
|
||||||
this.addHTMLProp(dlNodeName, tagName, key, value, dynamic, dependencyIndexArr, dependenciesNode)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (props.didUpdate) {
|
|
||||||
this.addUpdateStatements(allDependencyIndexArr, this.addOnUpdate(dlNodeName, props.didUpdate.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Resolve children
|
|
||||||
const childNames: string[] = [];
|
|
||||||
let mutable = false;
|
|
||||||
children.forEach((child, idx) => {
|
|
||||||
const [initStatements, childName] = this.generateChild(child);
|
|
||||||
childNames.push(childName);
|
|
||||||
this.addInitStatement(...initStatements);
|
|
||||||
if (child.type === 'html') {
|
|
||||||
this.addInitStatement(this.appendChild(dlNodeName, childName));
|
|
||||||
} else {
|
|
||||||
mutable = true;
|
|
||||||
this.addInitStatement(this.insertNode(dlNodeName, childName, idx));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (mutable) this.addInitStatement(this.setHTMLNodes(dlNodeName, childNames));
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} = createElement(${tag})
|
|
||||||
*/
|
|
||||||
private declareHTMLNode(dlNodeName: string, tag: t.Expression): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.createElement), [tag])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}._$nodes = [...${childNames}]
|
|
||||||
*/
|
|
||||||
private setHTMLNodes(dlNodeName: string, childNames: string[]): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$nodes')),
|
|
||||||
this.t.arrayExpression(childNames.map(name => this.t.identifier(name)))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}.appendChild(${childNodeName})
|
|
||||||
*/
|
|
||||||
private appendChild(dlNodeName: string, childNodeName: string): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('appendChild')), [
|
|
||||||
this.t.identifier(childNodeName),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { type IfParticle, type IfBranch } from '@openinula/reactivity-parser';
|
|
||||||
import CondGenerator from '../HelperGenerators/CondGenerator';
|
|
||||||
|
|
||||||
export default class IfGenerator extends CondGenerator {
|
|
||||||
run() {
|
|
||||||
const { branches } = this.viewParticle as IfParticle;
|
|
||||||
const deps = branches.flatMap(({ condition }) => condition.dependencyIndexArr ?? []);
|
|
||||||
|
|
||||||
// ---- declareIfNode
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
this.addInitStatement(this.declareIfNode(dlNodeName, branches, deps));
|
|
||||||
|
|
||||||
this.addUpdateStatements(deps, this.updateCondNodeCond(dlNodeName));
|
|
||||||
this.addUpdateStatementsWithoutDep(this.updateCondNode(dlNodeName));
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* if (${test}) { ${body} } else { ${alternate} }
|
|
||||||
*/
|
|
||||||
geneIfStatement(test: t.Expression, body: t.Statement[], alternate: t.Statement): t.IfStatement {
|
|
||||||
return this.t.ifStatement(test, this.t.blockStatement(body), alternate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* const ${dlNodeName} = new IfNode(($thisCond) => {
|
|
||||||
* if (cond1) {
|
|
||||||
* if ($thisCond.cond === 0) return
|
|
||||||
* ${children}
|
|
||||||
* $thisCond.cond = 0
|
|
||||||
* return [nodes]
|
|
||||||
* } else if (cond2) {
|
|
||||||
* if ($thisCond.cond === 1) return
|
|
||||||
* ${children}
|
|
||||||
* $thisCond.cond = 1
|
|
||||||
* return [nodes]
|
|
||||||
* }
|
|
||||||
* })
|
|
||||||
*/
|
|
||||||
private declareIfNode(dlNodeName: string, branches: IfBranch[], deps: number[]): t.Statement {
|
|
||||||
// ---- If no else statement, add one
|
|
||||||
if (
|
|
||||||
!this.t.isBooleanLiteral(branches[branches.length - 1].condition.value, {
|
|
||||||
value: true,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
branches.push({
|
|
||||||
condition: {
|
|
||||||
value: this.t.booleanLiteral(true),
|
|
||||||
dependencyIndexArr: [],
|
|
||||||
dependenciesNode: this.t.arrayExpression([]),
|
|
||||||
dynamic: false,
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const ifStatement = branches.reverse().reduce<any>((acc, { condition, children }, i) => {
|
|
||||||
const idx = branches.length - i - 1;
|
|
||||||
// ---- Generate children
|
|
||||||
const [childStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren(children, false, true);
|
|
||||||
|
|
||||||
// ---- Even if no updateStatements, we still need reassign an empty updateFunc
|
|
||||||
// to overwrite the previous one
|
|
||||||
/**
|
|
||||||
* $thisCond.updateFunc = (changed) => { ${updateStatements} }
|
|
||||||
*/
|
|
||||||
const updateNode = this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('updateFunc')),
|
|
||||||
this.t.arrowFunctionExpression(this.updateParams, this.geneUpdateBody(updateStatements))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// ---- Update func
|
|
||||||
childStatements.unshift(...this.declareNodes(nodeIdx), updateNode);
|
|
||||||
|
|
||||||
// ---- Check cond and update cond
|
|
||||||
childStatements.unshift(this.geneCondCheck(idx), this.geneCondIdx(idx));
|
|
||||||
|
|
||||||
// ---- Return statement
|
|
||||||
childStatements.push(this.geneCondReturnStatement(topLevelNodes, idx));
|
|
||||||
|
|
||||||
// ---- else statement
|
|
||||||
if (i === 0) return this.t.blockStatement(childStatements);
|
|
||||||
|
|
||||||
return this.geneIfStatement(condition.value, childStatements, acc);
|
|
||||||
}, undefined);
|
|
||||||
|
|
||||||
return this.declareCondNode(dlNodeName, this.t.blockStatement([ifStatement]), deps);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
import { type DependencyProp, type SnippetParticle } from '@openinula/reactivity-parser';
|
|
||||||
import type { types as t } from '@babel/core';
|
|
||||||
import PropViewGenerator from '../HelperGenerators/PropViewGenerator';
|
|
||||||
|
|
||||||
export default class SnippetGenerator extends PropViewGenerator {
|
|
||||||
run() {
|
|
||||||
let { props } = this.viewParticle as SnippetParticle;
|
|
||||||
props = this.alterPropViews(props);
|
|
||||||
const { tag } = this.viewParticle as SnippetParticle;
|
|
||||||
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
|
|
||||||
const availableProperties = this.snippetPropMap[tag] ?? [];
|
|
||||||
|
|
||||||
const allDependenciesNode: (t.ArrayExpression | t.NullLiteral)[] = Array.from(
|
|
||||||
{
|
|
||||||
length: availableProperties.length,
|
|
||||||
},
|
|
||||||
() => this.t.nullLiteral()
|
|
||||||
);
|
|
||||||
|
|
||||||
const allDependencyIndexArr: number[] = [];
|
|
||||||
|
|
||||||
Object.entries(props).forEach(([key, { value, dependencyIndexArr, dependenciesNode }]) => {
|
|
||||||
if (key === 'didUpdate') return;
|
|
||||||
if (SnippetGenerator.lifecycle.includes(key as (typeof SnippetGenerator.lifecycle)[number])) {
|
|
||||||
this.addInitStatement(this.addLifecycle(dlNodeName, key as (typeof SnippetGenerator.lifecycle)[number], value));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!dependencyIndexArr || dependencyIndexArr.length === 0) return;
|
|
||||||
allDependencyIndexArr.push(...dependencyIndexArr);
|
|
||||||
const depIdx = availableProperties.indexOf(key);
|
|
||||||
if (dependenciesNode) allDependenciesNode[depIdx] = dependenciesNode;
|
|
||||||
const propChange = 1 << depIdx;
|
|
||||||
this.addUpdateStatements(
|
|
||||||
dependencyIndexArr,
|
|
||||||
this.updateProp(dlNodeName, propChange, key, value, allDependenciesNode[depIdx])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (props.didUpdate) {
|
|
||||||
this.addUpdateStatements(allDependencyIndexArr, this.addOnUpdate(dlNodeName, props.didUpdate.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addInitStatement(...this.declareSnippetNode(dlNodeName, tag, props, allDependenciesNode));
|
|
||||||
|
|
||||||
this.addUpdateStatementsWithoutDep(this.updateSnippet(dlNodeName));
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* { ${key}: ${value}, ... }
|
|
||||||
*/
|
|
||||||
private genePropNode(props: Record<string, DependencyProp>): t.Expression {
|
|
||||||
return this.t.objectExpression(
|
|
||||||
Object.entries(props).map(([key, prop]) => {
|
|
||||||
return this.t.objectProperty(this.t.identifier(key), prop.value);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} = new SnippetNode(${allDependenciesNode})
|
|
||||||
* this.${tag}({${props}}, ${dlNodeName})
|
|
||||||
*/
|
|
||||||
private declareSnippetNode(
|
|
||||||
dlNodeName: string,
|
|
||||||
tag: string,
|
|
||||||
props: Record<string, DependencyProp>,
|
|
||||||
allDependenciesNode: (t.ArrayExpression | t.NullLiteral)[]
|
|
||||||
): t.Statement[] {
|
|
||||||
return [
|
|
||||||
this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.newExpression(this.t.identifier(this.importMap.SnippetNode), [
|
|
||||||
this.t.arrayExpression(allDependenciesNode),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
),
|
|
||||||
this.t.expressionStatement(
|
|
||||||
this.t.callExpression(this.t.memberExpression(this.t.thisExpression(), this.t.identifier(tag)), [
|
|
||||||
this.genePropNode(props),
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
])
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}.updateProp?.(${propChanged}, ...updateParams, () => { ${key}: ${value} }, allDependenciesNode)
|
|
||||||
*/
|
|
||||||
private updateProp(
|
|
||||||
dlNodeName: string,
|
|
||||||
propChanged: number,
|
|
||||||
key: string,
|
|
||||||
value: t.Expression,
|
|
||||||
allDependenciesNode: t.ArrayExpression | t.NullLiteral
|
|
||||||
): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.optionalCallExpression(
|
|
||||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('updateProp')),
|
|
||||||
[
|
|
||||||
this.t.numericLiteral(propChanged),
|
|
||||||
...this.updateParams.slice(1),
|
|
||||||
this.t.arrowFunctionExpression(
|
|
||||||
[],
|
|
||||||
this.t.objectExpression([this.t.objectProperty(this.t.identifier(key), value)])
|
|
||||||
),
|
|
||||||
allDependenciesNode,
|
|
||||||
],
|
|
||||||
true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}.update(changed)
|
|
||||||
*/
|
|
||||||
private updateSnippet(dlNodeName: string): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.optionalCallExpression(
|
|
||||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('update')),
|
|
||||||
this.updateParams,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { SwitchBranch, SwitchParticle } from '@openinula/reactivity-parser';
|
|
||||||
import CondGenerator from '../HelperGenerators/CondGenerator';
|
|
||||||
|
|
||||||
export default class SwitchGenerator extends CondGenerator {
|
|
||||||
run() {
|
|
||||||
const { branches, discriminant } = this.viewParticle as SwitchParticle;
|
|
||||||
|
|
||||||
const deps = branches.flatMap(({ case: _case }) => _case.dependencyIndexArr);
|
|
||||||
deps.push(...discriminant.dependencyIndexArr);
|
|
||||||
|
|
||||||
// ---- declareSwitchNode
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
this.addInitStatement(this.declareSwitchNode(dlNodeName, discriminant.value, branches, deps));
|
|
||||||
|
|
||||||
this.addUpdateStatements(deps, this.updateCondNodeCond(dlNodeName));
|
|
||||||
this.addUpdateStatementsWithoutDep(this.updateCondNode(dlNodeName));
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* const ${dlNodeName} = new CondNode(($thisCond) => {
|
|
||||||
* switch ($discriminant) {
|
|
||||||
* case ${case0}:
|
|
||||||
* if ($thisCond.case === 0) return
|
|
||||||
* case ${case1}:
|
|
||||||
* if ($thisCond.case === 1) return
|
|
||||||
* return [...${case1Nodes}]
|
|
||||||
* default:
|
|
||||||
* if ($thisCond.case === 2) return
|
|
||||||
* }
|
|
||||||
* _$nodes[0]._$updateFunc = (changed) => {
|
|
||||||
* _$updates.forEach(update => update(changed))
|
|
||||||
* })
|
|
||||||
* return _$nodes
|
|
||||||
* })
|
|
||||||
*/
|
|
||||||
private declareSwitchNode(
|
|
||||||
dlNodeName: string,
|
|
||||||
discriminant: t.Expression,
|
|
||||||
branches: SwitchBranch[],
|
|
||||||
deps: number[]
|
|
||||||
): t.Statement {
|
|
||||||
// ---- Format statements, make fallthrough statements append to the previous case
|
|
||||||
const formattedBranches: SwitchBranch[] = branches.map(({ case: _case, break: _break, children }, idx) => {
|
|
||||||
if (!_break) {
|
|
||||||
for (let i = idx + 1; i < branches.length; i++) {
|
|
||||||
children.push(...branches[i].children);
|
|
||||||
if (branches[i].break) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { case: _case, break: _break, children };
|
|
||||||
});
|
|
||||||
// ---- Add default case
|
|
||||||
const defaultCaseIdx = formattedBranches.findIndex(({ case: _case }) => _case === null);
|
|
||||||
if (defaultCaseIdx === -1) {
|
|
||||||
formattedBranches.push({
|
|
||||||
case: {
|
|
||||||
value: this.t.booleanLiteral(true),
|
|
||||||
dependencyIndexArr: [],
|
|
||||||
dependenciesNode: this.t.arrayExpression([]),
|
|
||||||
dynamic: false,
|
|
||||||
},
|
|
||||||
break: true,
|
|
||||||
children: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const switchStatements = formattedBranches.map(({ case: _case, children }, idx) => {
|
|
||||||
// ---- Generate case statements
|
|
||||||
const [childStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren(children, false, true);
|
|
||||||
|
|
||||||
// ---- Even if no updateStatements, we still need reassign an empty updateFunc
|
|
||||||
// to overwrite the previous one
|
|
||||||
/**
|
|
||||||
* $thisCond.updateFunc = (changed) => { ${updateStatements} }
|
|
||||||
*/
|
|
||||||
const updateNode = this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('updateFunc')),
|
|
||||||
this.t.arrowFunctionExpression(this.updateParams, this.geneUpdateBody(updateStatements))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// ---- Update func
|
|
||||||
childStatements.unshift(...this.declareNodes(nodeIdx), updateNode);
|
|
||||||
|
|
||||||
// ---- Check cond and update cond
|
|
||||||
childStatements.unshift(this.geneCondCheck(idx), this.geneCondIdx(idx));
|
|
||||||
|
|
||||||
// ---- Return statement
|
|
||||||
childStatements.push(this.geneCondReturnStatement(topLevelNodes, idx));
|
|
||||||
|
|
||||||
return this.t.switchCase(_case ? _case.value : null, [this.t.blockStatement(childStatements)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.declareCondNode(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.blockStatement([this.t.switchStatement(discriminant, switchStatements)]),
|
|
||||||
deps
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,282 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { type HTMLParticle, type TemplateParticle } from '@openinula/reactivity-parser';
|
|
||||||
import HTMLPropGenerator from '../HelperGenerators/HTMLPropGenerator';
|
|
||||||
|
|
||||||
export default class TemplateGenerator extends HTMLPropGenerator {
|
|
||||||
run() {
|
|
||||||
const { template, mutableParticles, props } = this.viewParticle as TemplateParticle;
|
|
||||||
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
// ---- Add template declaration to class
|
|
||||||
const templateName = this.addTemplate(template);
|
|
||||||
// ---- Declare template node in View body
|
|
||||||
this.addInitStatement(this.declareTemplateNode(dlNodeName, templateName));
|
|
||||||
|
|
||||||
// ---- Insert elements first
|
|
||||||
const paths: number[][] = [];
|
|
||||||
props.forEach(({ path }) => {
|
|
||||||
paths.push(path);
|
|
||||||
});
|
|
||||||
mutableParticles.forEach(({ path }) => {
|
|
||||||
paths.push(path.slice(0, -1));
|
|
||||||
});
|
|
||||||
const [insertElementStatements, pathNameMap] = this.insertElements(paths, dlNodeName);
|
|
||||||
this.addInitStatement(...insertElementStatements);
|
|
||||||
|
|
||||||
// ---- Resolve props
|
|
||||||
const didUpdateMap: Record<string, { deps: number[]; value?: t.Expression }> = {};
|
|
||||||
props.forEach(({ tag, path, key, value, dependencyIndexArr, dependenciesNode, dynamic }) => {
|
|
||||||
const name = pathNameMap[path.join('.')];
|
|
||||||
if (!didUpdateMap[name])
|
|
||||||
didUpdateMap[name] = {
|
|
||||||
deps: [],
|
|
||||||
};
|
|
||||||
if (key === 'didUpdate') {
|
|
||||||
didUpdateMap[name].value = value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
didUpdateMap[name].deps.push(...dependencyIndexArr);
|
|
||||||
|
|
||||||
this.addInitStatement(this.addHTMLProp(name, tag, key, value, dynamic, dependencyIndexArr, dependenciesNode));
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.entries(didUpdateMap).forEach(([name, { deps, value }]) => {
|
|
||||||
if (!value) return;
|
|
||||||
this.addUpdateStatements(deps, this.addOnUpdate(name, value));
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- Resolve mutable particles
|
|
||||||
mutableParticles.forEach(particle => {
|
|
||||||
const path = particle.path;
|
|
||||||
// ---- Find parent htmlElement
|
|
||||||
const parentName = pathNameMap[path.slice(0, -1).join('.')];
|
|
||||||
const [initStatements, childName] = this.generateChild(particle);
|
|
||||||
this.addInitStatement(...initStatements);
|
|
||||||
this.addInitStatement(this.insertNode(parentName, childName, path[path.length - 1]));
|
|
||||||
});
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* static ${templateName} = (() => {
|
|
||||||
* let _$node0, _$node1, ...
|
|
||||||
* ${template}
|
|
||||||
*
|
|
||||||
* return _$node0
|
|
||||||
* })()
|
|
||||||
*/
|
|
||||||
private addTemplate(template: HTMLParticle): string {
|
|
||||||
const templateName = this.generateTemplateName();
|
|
||||||
const [statements, nodeName, , nodeIdx] = this.generateChild(template, false, true);
|
|
||||||
this.addStaticClassProperty(
|
|
||||||
templateName,
|
|
||||||
this.t.callExpression(
|
|
||||||
this.t.arrowFunctionExpression(
|
|
||||||
[],
|
|
||||||
this.t.blockStatement([
|
|
||||||
...this.declareNodes(nodeIdx),
|
|
||||||
...statements,
|
|
||||||
this.t.returnStatement(this.t.identifier(nodeName)),
|
|
||||||
])
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return templateName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} = ${this.className}.${templateName}.cloneNode(true)
|
|
||||||
*/
|
|
||||||
private declareTemplateNode(dlNodeName: string, templateName: string): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.callExpression(
|
|
||||||
this.t.memberExpression(
|
|
||||||
this.t.memberExpression(this.t.identifier(this.className), this.t.identifier(templateName)),
|
|
||||||
this.t.identifier('cloneNode')
|
|
||||||
),
|
|
||||||
[this.t.booleanLiteral(true)]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}.firstChild
|
|
||||||
* or
|
|
||||||
* ${dlNodeName}.firstChild.nextSibling
|
|
||||||
* or
|
|
||||||
* ...
|
|
||||||
* ${dlNodeName}.childNodes[${num}]
|
|
||||||
*/
|
|
||||||
private insertElement(dlNodeName: string, path: number[], offset: number): t.Statement {
|
|
||||||
const newNodeName = this.generateNodeName();
|
|
||||||
if (path.length === 0) {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(newNodeName),
|
|
||||||
Array.from({ length: offset }).reduce(
|
|
||||||
(acc: t.Expression) => this.t.memberExpression(acc, this.t.identifier('nextSibling')),
|
|
||||||
this.t.identifier(dlNodeName)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const addFirstChild = (object: t.Expression) =>
|
|
||||||
// ---- ${object}.firstChild
|
|
||||||
this.t.memberExpression(object, this.t.identifier('firstChild'));
|
|
||||||
const addSecondChild = (object: t.Expression) =>
|
|
||||||
// ---- ${object}.firstChild.nextSibling
|
|
||||||
this.t.memberExpression(addFirstChild(object), this.t.identifier('nextSibling'));
|
|
||||||
const addThirdChild = (object: t.Expression) =>
|
|
||||||
// ---- ${object}.firstChild.nextSibling.nextSibling
|
|
||||||
this.t.memberExpression(addSecondChild(object), this.t.identifier('nextSibling'));
|
|
||||||
const addOtherChild = (object: t.Expression, num: number) =>
|
|
||||||
// ---- ${object}.childNodes[${num}]
|
|
||||||
this.t.memberExpression(
|
|
||||||
this.t.memberExpression(object, this.t.identifier('childNodes')),
|
|
||||||
this.t.numericLiteral(num),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
const addNextSibling = (object: t.Expression) =>
|
|
||||||
// ---- ${object}.nextSibling
|
|
||||||
this.t.memberExpression(object, this.t.identifier('nextSibling'));
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(newNodeName),
|
|
||||||
path.reduce((acc: t.Expression, cur: number, idx) => {
|
|
||||||
if (idx === 0 && offset > 0) {
|
|
||||||
for (let i = 0; i < offset; i++) acc = addNextSibling(acc);
|
|
||||||
}
|
|
||||||
if (cur === 0) return addFirstChild(acc);
|
|
||||||
if (cur === 1) return addSecondChild(acc);
|
|
||||||
if (cur === 2) return addThirdChild(acc);
|
|
||||||
return addOtherChild(acc, cur);
|
|
||||||
}, this.t.identifier(dlNodeName))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Insert elements to the template node from the paths
|
|
||||||
* @param paths
|
|
||||||
* @param dlNodeName
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
private insertElements(paths: number[][], dlNodeName: string): [t.Statement[], Record<string, string>] {
|
|
||||||
const [statements, collect] = HTMLPropGenerator.statementsCollector();
|
|
||||||
const nameMap: Record<string, number[]> = { [dlNodeName]: [] };
|
|
||||||
|
|
||||||
const commonPrefixPaths = TemplateGenerator.pathWithCommonPrefix(paths);
|
|
||||||
|
|
||||||
commonPrefixPaths.forEach(path => {
|
|
||||||
const res = TemplateGenerator.findBestNodeAndPath(nameMap, path, dlNodeName);
|
|
||||||
const [, pat, offset] = res;
|
|
||||||
let name = res[0];
|
|
||||||
|
|
||||||
if (pat.length !== 0 || offset !== 0) {
|
|
||||||
collect(this.insertElement(name, pat, offset));
|
|
||||||
name = this.generateNodeName(this.nodeIdx);
|
|
||||||
nameMap[name] = path;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const pathNameMap = Object.fromEntries(Object.entries(nameMap).map(([name, path]) => [path.join('.'), name]));
|
|
||||||
|
|
||||||
return [statements, pathNameMap];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Path related
|
|
||||||
/**
|
|
||||||
* @brief Extract common prefix from paths
|
|
||||||
* e.g.
|
|
||||||
* [0, 1, 2, 3] + [0, 1, 2, 4] => [0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 4]
|
|
||||||
* [0, 1, 2] is the common prefix
|
|
||||||
* @param paths
|
|
||||||
* @returns paths with common prefix
|
|
||||||
*/
|
|
||||||
private static pathWithCommonPrefix(paths: number[][]): number[][] {
|
|
||||||
const allPaths = [...paths];
|
|
||||||
paths.forEach(path0 => {
|
|
||||||
paths.forEach(path1 => {
|
|
||||||
if (path0 === path1) return;
|
|
||||||
for (let i = 0; i < path0.length; i++) {
|
|
||||||
if (path0[i] !== path1[i]) {
|
|
||||||
if (i !== 0) {
|
|
||||||
allPaths.push(path0.slice(0, i));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- Sort by length and then by first element, small to large
|
|
||||||
const sortedPaths = allPaths.sort((a, b) => {
|
|
||||||
if (a.length !== b.length) return a.length - b.length;
|
|
||||||
return a[0] - b[0];
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- Deduplicate
|
|
||||||
const deduplicatedPaths = [...new Set(sortedPaths.map(path => path.join('.')))].map(path =>
|
|
||||||
path.split('.').filter(Boolean).map(Number)
|
|
||||||
);
|
|
||||||
|
|
||||||
return deduplicatedPaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Find the best node name and path for the given path by looking into the nameMap.
|
|
||||||
* If there's a full match, return the name and an empty path
|
|
||||||
* If there's a partly match, return the name and the remaining path
|
|
||||||
* If there's a nextSibling match, return the name and the remaining path with sibling offset
|
|
||||||
* @param nameMap
|
|
||||||
* @param path
|
|
||||||
* @param defaultName
|
|
||||||
* @returns [name, path, siblingOffset]
|
|
||||||
*/
|
|
||||||
private static findBestNodeAndPath(
|
|
||||||
nameMap: Record<string, number[]>,
|
|
||||||
path: number[],
|
|
||||||
defaultName: string
|
|
||||||
): [string, number[], number] {
|
|
||||||
let bestMatchCount = 0;
|
|
||||||
let bestMatchName: string | undefined;
|
|
||||||
let bestHalfMatch: [string, number, number] | undefined;
|
|
||||||
Object.entries(nameMap).forEach(([name, pat]) => {
|
|
||||||
let matchCount = 0;
|
|
||||||
const pathLength = pat.length;
|
|
||||||
for (let i = 0; i < pathLength; i++) {
|
|
||||||
if (pat[i] === path[i]) matchCount++;
|
|
||||||
}
|
|
||||||
if (matchCount === pathLength - 1) {
|
|
||||||
const offset = path[pathLength - 1] - pat[pathLength - 1];
|
|
||||||
if (offset > 0 && offset <= 3) {
|
|
||||||
bestHalfMatch = [name, matchCount, offset];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (matchCount !== pat.length) return;
|
|
||||||
if (matchCount > bestMatchCount) {
|
|
||||||
bestMatchName = name;
|
|
||||||
bestMatchCount = matchCount;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!bestMatchName) {
|
|
||||||
if (bestHalfMatch) {
|
|
||||||
return [bestHalfMatch[0], path.slice(bestHalfMatch[1] + 1), bestHalfMatch[2]];
|
|
||||||
}
|
|
||||||
return [defaultName, path, 0];
|
|
||||||
}
|
|
||||||
return [bestMatchName, path.slice(bestMatchCount), 0];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { type TextParticle } from '@openinula/reactivity-parser';
|
|
||||||
import BaseGenerator from '../HelperGenerators/BaseGenerator';
|
|
||||||
|
|
||||||
export default class TextGenerator extends BaseGenerator {
|
|
||||||
run() {
|
|
||||||
const { content } = this.viewParticle as TextParticle;
|
|
||||||
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
|
|
||||||
this.addInitStatement(this.declareTextNode(dlNodeName, content.value, content.dependenciesNode));
|
|
||||||
|
|
||||||
if (content.dynamic) {
|
|
||||||
this.addUpdateStatements(
|
|
||||||
content.dependencyIndexArr,
|
|
||||||
this.updateTextNode(dlNodeName, content.value, content.dependenciesNode)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} = createTextNode(${value}, ${deps})
|
|
||||||
*/
|
|
||||||
private declareTextNode(dlNodeName: string, value: t.Expression, dependenciesNode: t.Expression): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.createTextNode), [value, dependenciesNode])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} && updateText(${dlNodeName}, () => ${value}, ${deps})
|
|
||||||
*/
|
|
||||||
private updateTextNode(dlNodeName: string, value: t.Expression, dependenciesNode: t.Expression): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.logicalExpression(
|
|
||||||
'&&',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.callExpression(this.t.identifier(this.importMap.updateText), [
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.arrowFunctionExpression([], value),
|
|
||||||
dependenciesNode,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import BaseGenerator from '../HelperGenerators/BaseGenerator';
|
|
||||||
import { TryParticle, type ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
|
|
||||||
export default class TryGenerator extends BaseGenerator {
|
|
||||||
run() {
|
|
||||||
const { children, catchChildren, exception } = this.viewParticle as TryParticle;
|
|
||||||
|
|
||||||
const dlNodeName = this.generateNodeName();
|
|
||||||
|
|
||||||
// ---- Declare for node
|
|
||||||
this.addInitStatement(this.declareTryNode(dlNodeName, children, catchChildren, exception));
|
|
||||||
|
|
||||||
// ---- Update statements
|
|
||||||
this.addUpdateStatementsWithoutDep(this.declareUpdate(dlNodeName));
|
|
||||||
|
|
||||||
return dlNodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* $setUpdate($catchable(updateStatements))
|
|
||||||
* ${children}
|
|
||||||
* return [...${topLevelNodes}]
|
|
||||||
*/
|
|
||||||
private declareTryNodeUpdate(children: ViewParticle[], addCatchable = true): t.Statement[] {
|
|
||||||
const [childStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren(children, false, true);
|
|
||||||
|
|
||||||
const updateFunc = this.t.arrowFunctionExpression(
|
|
||||||
[this.t.identifier('$changed')],
|
|
||||||
this.geneUpdateBody(updateStatements)
|
|
||||||
);
|
|
||||||
|
|
||||||
childStatements.unshift(
|
|
||||||
...this.declareNodes(nodeIdx),
|
|
||||||
this.t.expressionStatement(
|
|
||||||
this.t.callExpression(
|
|
||||||
this.t.identifier('$setUpdate'),
|
|
||||||
addCatchable ? [this.t.callExpression(this.t.identifier('$catchable'), [updateFunc])] : [updateFunc]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
childStatements.push(
|
|
||||||
this.t.returnStatement(this.t.arrayExpression(topLevelNodes.map(node => this.t.identifier(node))))
|
|
||||||
);
|
|
||||||
|
|
||||||
return childStatements;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName} = new TryNode(($setUpdate, $catchable) => {
|
|
||||||
* ${children}
|
|
||||||
* }, ($setUpdate, e) => {
|
|
||||||
* ${catchChildren}
|
|
||||||
* })
|
|
||||||
* })
|
|
||||||
*/
|
|
||||||
private declareTryNode(
|
|
||||||
dlNodeName: string,
|
|
||||||
children: ViewParticle[],
|
|
||||||
catchChildren: ViewParticle[],
|
|
||||||
exception: TryParticle['exception']
|
|
||||||
): t.Statement {
|
|
||||||
const exceptionNodes = exception ? [exception] : [];
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.identifier(dlNodeName),
|
|
||||||
this.t.newExpression(this.t.identifier(this.importMap.TryNode), [
|
|
||||||
this.t.arrowFunctionExpression(
|
|
||||||
[this.t.identifier('$setUpdate'), this.t.identifier('$catchable')],
|
|
||||||
this.t.blockStatement(this.declareTryNodeUpdate(children, true))
|
|
||||||
),
|
|
||||||
this.t.arrowFunctionExpression(
|
|
||||||
[this.t.identifier('$setUpdate'), ...exceptionNodes],
|
|
||||||
this.t.blockStatement(this.declareTryNodeUpdate(catchChildren, false))
|
|
||||||
),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* ${dlNodeName}?.update(changed)
|
|
||||||
*/
|
|
||||||
private declareUpdate(dlNodeName: string): t.Statement {
|
|
||||||
return this.optionalExpression(
|
|
||||||
dlNodeName,
|
|
||||||
this.t.callExpression(
|
|
||||||
this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('update')),
|
|
||||||
this.updateParams
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,184 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { type ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
import ViewGenerator from './ViewGenerator';
|
|
||||||
|
|
||||||
export default class SnippetGenerator extends ViewGenerator {
|
|
||||||
/**
|
|
||||||
* @brief Generate the snippet, i.e., @View MySnippet({ prop1, prop2 }) { ... }
|
|
||||||
* This is different from the main view in that it has a props node
|
|
||||||
* and is needed to parse twice,
|
|
||||||
* 1. for this.deps (viewParticlesWithPropertyDep)
|
|
||||||
* 2. for props that passed in this snippet (viewParticlesWithIdentityDep)
|
|
||||||
* @param viewParticlesWithPropertyDep
|
|
||||||
* @param viewParticlesWithIdentityDep
|
|
||||||
* @param propsNode
|
|
||||||
* @returns [viewBody, classProperties, templateIdx]
|
|
||||||
*/
|
|
||||||
generate(
|
|
||||||
viewParticlesWithPropertyDep: ViewParticle[],
|
|
||||||
viewParticlesWithIdentityDep: ViewParticle[],
|
|
||||||
propsNode: t.ObjectPattern
|
|
||||||
): [t.BlockStatement, t.ClassProperty[], number] {
|
|
||||||
const allClassProperties: t.ClassProperty[] = [];
|
|
||||||
const allInitStatements: t.Statement[] = [];
|
|
||||||
const propertyUpdateStatements: Record<number, t.Statement[]> = {};
|
|
||||||
const identifierUpdateStatements: Record<number, t.Statement[]> = {};
|
|
||||||
const topLevelNodes: string[] = [];
|
|
||||||
|
|
||||||
const templateIdx = this.templateIdx;
|
|
||||||
viewParticlesWithPropertyDep.forEach(viewParticle => {
|
|
||||||
const [initStatements, updateStatements, classProperties, nodeName] = this.generateChild(viewParticle);
|
|
||||||
allInitStatements.push(...initStatements);
|
|
||||||
Object.entries(updateStatements).forEach(([depNum, statements]) => {
|
|
||||||
if (!propertyUpdateStatements[Number(depNum)]) {
|
|
||||||
propertyUpdateStatements[Number(depNum)] = [];
|
|
||||||
}
|
|
||||||
propertyUpdateStatements[Number(depNum)].push(...statements);
|
|
||||||
});
|
|
||||||
allClassProperties.push(...classProperties);
|
|
||||||
topLevelNodes.push(nodeName);
|
|
||||||
});
|
|
||||||
// ---- Recover the templateIdx and reinitialize the nodeIdx
|
|
||||||
this.templateIdx = templateIdx;
|
|
||||||
this.nodeIdx = -1;
|
|
||||||
viewParticlesWithIdentityDep.forEach(viewParticle => {
|
|
||||||
// ---- We only need the update statements for the second props parsing
|
|
||||||
// because all the init statements are already generated
|
|
||||||
// a little bit time consuming but otherwise we need to write two different generators
|
|
||||||
const [, updateStatements] = this.generateChild(viewParticle);
|
|
||||||
|
|
||||||
Object.entries(updateStatements).forEach(([depNum, statements]) => {
|
|
||||||
if (!identifierUpdateStatements[Number(depNum)]) {
|
|
||||||
identifierUpdateStatements[Number(depNum)] = [];
|
|
||||||
}
|
|
||||||
identifierUpdateStatements[Number(depNum)].push(...statements);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasPropertyUpdateFunc = Object.keys(propertyUpdateStatements).length > 0;
|
|
||||||
const hasIdentifierUpdateFunc = Object.keys(identifierUpdateStatements).filter(n => n !== '0').length > 0;
|
|
||||||
|
|
||||||
const viewBody = this.t.blockStatement([
|
|
||||||
...this.declareNodes(),
|
|
||||||
...(hasPropertyUpdateFunc ? [this.geneUpdateFunc('update', propertyUpdateStatements)] : []),
|
|
||||||
...(hasIdentifierUpdateFunc ? [this.geneUpdateFunc('updateProp', identifierUpdateStatements, propsNode)] : []),
|
|
||||||
...allInitStatements,
|
|
||||||
this.geneAddNodes(topLevelNodes),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [viewBody, allClassProperties, this.templateIdx];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* $snippetNode._$nodes = ${topLevelNodes}
|
|
||||||
*/
|
|
||||||
geneAddNodes(topLevelNodes: string[]): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.memberExpression(this.t.identifier('$snippetNode'), this.t.identifier('_$nodes')),
|
|
||||||
this.t.arrayExpression(topLevelNodes.map(nodeName => this.t.identifier(nodeName)))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* $snippetNode.${name} = (changed) => { ${updateStatements} }
|
|
||||||
*/
|
|
||||||
geneUpdateFunc(
|
|
||||||
name: string,
|
|
||||||
updateStatements: Record<number, t.Statement[]>,
|
|
||||||
propsNode?: t.ObjectPattern
|
|
||||||
): t.Statement {
|
|
||||||
return this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression(
|
|
||||||
'=',
|
|
||||||
this.t.memberExpression(this.t.identifier('$snippetNode'), this.t.identifier(name)),
|
|
||||||
this.geneUpdateBody(updateStatements, propsNode)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* (changed) => {
|
|
||||||
* if (changed & 1) {
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
private geneUpdateBody(
|
|
||||||
updateStatements: Record<number, t.Statement[]>,
|
|
||||||
propsNode?: t.ObjectPattern
|
|
||||||
): t.ArrowFunctionExpression {
|
|
||||||
const bodyEntryNodes: t.Statement[] = [];
|
|
||||||
// ---- Args
|
|
||||||
const args: t.Identifier[] = this.updateParams;
|
|
||||||
if (propsNode) {
|
|
||||||
// ---- Add $snippetProps and $depsArr to args
|
|
||||||
args.push(this.t.identifier('$snippetPropsFunc'), this.t.identifier('$depsArr'));
|
|
||||||
|
|
||||||
// ---- Add cache
|
|
||||||
/**
|
|
||||||
* if ($snippetNode.cached(depsArr, changed)) return
|
|
||||||
*/
|
|
||||||
bodyEntryNodes.push(
|
|
||||||
this.t.ifStatement(
|
|
||||||
this.t.callExpression(
|
|
||||||
this.t.memberExpression(this.t.identifier('$snippetNode'), this.t.identifier('cached')),
|
|
||||||
[this.t.identifier('$depsArr'), this.t.identifier('$changed')]
|
|
||||||
),
|
|
||||||
this.t.blockStatement([this.t.returnStatement()])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* const $snippetProps = $snippetPropsFunc()
|
|
||||||
*/
|
|
||||||
bodyEntryNodes.push(
|
|
||||||
this.t.variableDeclaration('const', [
|
|
||||||
this.t.variableDeclarator(
|
|
||||||
this.t.identifier('$snippetProps'),
|
|
||||||
this.t.callExpression(this.t.identifier('$snippetPropsFunc'), [])
|
|
||||||
),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ${prop} = $snippetProps
|
|
||||||
*/
|
|
||||||
propsNode.properties
|
|
||||||
.filter(prop => this.t.isObjectProperty(prop))
|
|
||||||
.forEach((prop, idx) => {
|
|
||||||
const depNum = 1 << idx;
|
|
||||||
if (!updateStatements[depNum]) updateStatements[depNum] = [];
|
|
||||||
updateStatements[depNum].unshift(
|
|
||||||
this.t.expressionStatement(
|
|
||||||
this.t.assignmentExpression('=', this.t.objectPattern([prop]), this.t.identifier('$snippetProps'))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// ---- End
|
|
||||||
const runAllStatements = propsNode ? [] : updateStatements[0] ?? [];
|
|
||||||
|
|
||||||
return this.t.arrowFunctionExpression(
|
|
||||||
args,
|
|
||||||
this.t.blockStatement([
|
|
||||||
...bodyEntryNodes,
|
|
||||||
...Object.entries(updateStatements)
|
|
||||||
.filter(([depNum]) => depNum !== '0')
|
|
||||||
.map(([depNum, statements]) => {
|
|
||||||
return this.t.ifStatement(
|
|
||||||
this.t.binaryExpression('&', this.t.identifier('$changed'), this.t.numericLiteral(Number(depNum))),
|
|
||||||
this.t.blockStatement(statements)
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
...runAllStatements,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import { type ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
import { type ViewGeneratorConfig } from './types';
|
|
||||||
import BaseGenerator, { prefixMap } from './HelperGenerators/BaseGenerator';
|
|
||||||
import CompGenerator from './NodeGenerators/CompGenerator';
|
|
||||||
import HTMLGenerator from './NodeGenerators/HTMLGenerator';
|
|
||||||
import TemplateGenerator from './NodeGenerators/TemplateGenerator';
|
|
||||||
import ForGenerator from './NodeGenerators/ForGenerator';
|
|
||||||
import IfGenerator from './NodeGenerators/IfGenerator';
|
|
||||||
import EnvGenerator from './NodeGenerators/EnvGenerator';
|
|
||||||
import TextGenerator from './NodeGenerators/TextGenerator';
|
|
||||||
import ExpGenerator from './NodeGenerators/ExpGenerator';
|
|
||||||
import SnippetGenerator from './NodeGenerators/SnippetGenerator';
|
|
||||||
import SwitchGenerator from './NodeGenerators/SwitchGenerator';
|
|
||||||
import TryGenerator from './NodeGenerators/TryGenerator';
|
|
||||||
|
|
||||||
export default class ViewGenerator {
|
|
||||||
config: ViewGeneratorConfig;
|
|
||||||
t: typeof t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Construct the view generator from config
|
|
||||||
* @param config
|
|
||||||
*/
|
|
||||||
constructor(config: ViewGeneratorConfig) {
|
|
||||||
this.config = config;
|
|
||||||
this.t = config.babelApi.types;
|
|
||||||
this.templateIdx = config.templateIdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Different generator classes for different view particle types
|
|
||||||
*/
|
|
||||||
static generatorMap: Record<string, typeof BaseGenerator> = {
|
|
||||||
comp: CompGenerator,
|
|
||||||
html: HTMLGenerator,
|
|
||||||
template: TemplateGenerator,
|
|
||||||
for: ForGenerator,
|
|
||||||
if: IfGenerator,
|
|
||||||
switch: SwitchGenerator,
|
|
||||||
env: EnvGenerator,
|
|
||||||
text: TextGenerator,
|
|
||||||
exp: ExpGenerator,
|
|
||||||
snippet: SnippetGenerator,
|
|
||||||
try: TryGenerator,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Generate the view given the view particles, mainly used for child particles parsing
|
|
||||||
* @param viewParticles
|
|
||||||
* @returns [initStatements, updateStatements, classProperties, topLevelNodes]
|
|
||||||
*/
|
|
||||||
generateChildren(
|
|
||||||
viewParticles: ViewParticle[]
|
|
||||||
): [t.Statement[], Record<number, t.Statement[]>, t.ClassProperty[], string[]] {
|
|
||||||
const allInitStatements: t.Statement[] = [];
|
|
||||||
const allClassProperties: t.ClassProperty[] = [];
|
|
||||||
const allUpdateStatements: Record<number, t.Statement[]> = {};
|
|
||||||
const topLevelNodes: string[] = [];
|
|
||||||
|
|
||||||
viewParticles.forEach(viewParticle => {
|
|
||||||
const [initStatements, updateStatements, classProperties, nodeName] = this.generateChild(viewParticle);
|
|
||||||
allInitStatements.push(...initStatements);
|
|
||||||
Object.entries(updateStatements).forEach(([depNum, statements]) => {
|
|
||||||
if (!allUpdateStatements[Number(depNum)]) {
|
|
||||||
allUpdateStatements[Number(depNum)] = [];
|
|
||||||
}
|
|
||||||
allUpdateStatements[Number(depNum)].push(...statements);
|
|
||||||
});
|
|
||||||
allClassProperties.push(...classProperties);
|
|
||||||
topLevelNodes.push(nodeName);
|
|
||||||
});
|
|
||||||
|
|
||||||
return [allInitStatements, allUpdateStatements, allClassProperties, topLevelNodes];
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeIdx = -1;
|
|
||||||
templateIdx = -1;
|
|
||||||
/**
|
|
||||||
* @brief Generate the view given the view particle, using generator from the map
|
|
||||||
* @param viewParticle
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
generateChild(viewParticle: ViewParticle) {
|
|
||||||
const { type } = viewParticle;
|
|
||||||
const GeneratorClass = ViewGenerator.generatorMap[type];
|
|
||||||
if (!GeneratorClass) {
|
|
||||||
throw new Error(`Unknown view particle type: ${type}`);
|
|
||||||
}
|
|
||||||
const generator = new GeneratorClass(viewParticle, this.config);
|
|
||||||
generator.nodeIdx = this.nodeIdx;
|
|
||||||
generator.templateIdx = this.templateIdx;
|
|
||||||
const result = generator.generate();
|
|
||||||
this.nodeIdx = generator.nodeIdx;
|
|
||||||
this.templateIdx = generator.templateIdx;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @View
|
|
||||||
* let node1, node2, ...
|
|
||||||
*/
|
|
||||||
declareNodes(): t.Statement[] {
|
|
||||||
if (this.nodeIdx === -1) return [];
|
|
||||||
return [
|
|
||||||
this.t.variableDeclaration(
|
|
||||||
'let',
|
|
||||||
Array.from({ length: this.nodeIdx + 1 }, (_, i) =>
|
|
||||||
this.t.variableDeclarator(this.t.identifier(`${prefixMap.node}${i}`))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
get updateParams() {
|
|
||||||
return [this.t.identifier('$changed')];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { createErrorHandler } from '@openinula/error-handler';
|
|
||||||
|
|
||||||
export const DLError = createErrorHandler(
|
|
||||||
'ViewGenerator',
|
|
||||||
{
|
|
||||||
1: 'Element prop in HTML should be a function or an identifier',
|
|
||||||
2: 'Unrecognized HTML common prop',
|
|
||||||
3: 'Do prop only accepts function or arrow function',
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
1: 'ExpressionNode only supports prop as element and lifecycle, receiving $0',
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { type ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
import { type ViewGeneratorConfig } from './types';
|
|
||||||
import { type types as t } from '@babel/core';
|
|
||||||
import MainViewGenerator from './MainViewGenerator';
|
|
||||||
import SnippetGenerator from './SnippetGenerator';
|
|
||||||
|
|
||||||
export function generateView(
|
|
||||||
viewParticles: ViewParticle[],
|
|
||||||
config: ViewGeneratorConfig
|
|
||||||
): [t.BlockStatement, t.ClassProperty[], number] {
|
|
||||||
return new MainViewGenerator(config).generate(viewParticles);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateSnippet(
|
|
||||||
viewParticlesWithPropertyDep: ViewParticle[],
|
|
||||||
viewParticlesWithIdentityDep: ViewParticle[],
|
|
||||||
propNode: t.ObjectPattern,
|
|
||||||
config: ViewGeneratorConfig
|
|
||||||
): [t.BlockStatement, t.ClassProperty[], number] {
|
|
||||||
return new SnippetGenerator(config).generate(viewParticlesWithPropertyDep, viewParticlesWithIdentityDep, propNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from './types';
|
|
|
@ -1,12 +0,0 @@
|
||||||
import type Babel from '@babel/core';
|
|
||||||
|
|
||||||
export type SnippetPropMap = Record<string, string[]>;
|
|
||||||
export interface ViewGeneratorConfig {
|
|
||||||
babelApi: typeof Babel;
|
|
||||||
className: string;
|
|
||||||
importMap: Record<string, string>;
|
|
||||||
snippetPropMap: SnippetPropMap;
|
|
||||||
templateIdx: number;
|
|
||||||
attributeMap: Record<string, string[]>;
|
|
||||||
alterAttributeMap: Record<string, string>;
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ESNext", "DOM"],
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true
|
|
||||||
},
|
|
||||||
"ts-node": {
|
|
||||||
"esm": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Slidev
|
||||||
|
|
||||||
|
Hello, World!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Page 2
|
||||||
|
|
||||||
|
Directly use code blocks for highlighting
|
||||||
|
|
||||||
|
```ts
|
||||||
|
console.log('Hello, World!')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Page 3
|
||||||
|
|
||||||
|
You can directly use Windi CSS and Vue components to style and enrich your slides.
|
||||||
|
|
||||||
|
<div class="p-3">
|
||||||
|
<Tweet id="20" />
|
||||||
|
</div>
|
Loading…
Reference in New Issue