refactor(parser): prune unused bit
This commit is contained in:
parent
5fc41b8e5e
commit
4a877cdc34
|
@ -19,6 +19,7 @@
|
|||
"typings": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup --sourcemap",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -36,9 +37,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",
|
||||
|
|
|
@ -14,16 +14,18 @@
|
|||
*/
|
||||
|
||||
import { NodePath } from '@babel/core';
|
||||
import { LifeCycle, Visitor } from './types';
|
||||
import { addLifecycle, addWatch } from './nodeFactory';
|
||||
import { LifeCycle, Visitor } from '../types';
|
||||
import { addLifecycle, addWatch } from '../nodeFactory';
|
||||
import { types as t } from '@openinula/babel-api';
|
||||
import { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
||||
import { extractFnFromMacro, getFnBodyPath } from '../utils';
|
||||
import { getDependenciesFromNode } from './reactive/getDependencies';
|
||||
import { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../../constants';
|
||||
import { extractFnFromMacro, getFnBodyPath } from '../../utils';
|
||||
import { getDependenciesFromNode } from '@openinula/reactivity-parser';
|
||||
import { reactivityFuncNames } from '../../const';
|
||||
|
||||
function isLifeCycleName(name: string): name is LifeCycle {
|
||||
return [WILL_MOUNT, ON_MOUNT, WILL_UNMOUNT, ON_UNMOUNT].includes(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze the functional macro in the function component
|
||||
* 1. lifecycle
|
||||
|
@ -50,12 +52,16 @@ export function functionalMacroAnalyze(): Visitor {
|
|||
|
||||
// watch
|
||||
if (calleeName === WATCH) {
|
||||
const fnNode = extractFnFromMacro(expression, WATCH);
|
||||
const fnPath = extractFnFromMacro(expression, WATCH);
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -13,14 +13,15 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { Visitor } from './types';
|
||||
import { addMethod, addProperty, addSubComponent, createComponentNode } from './nodeFactory';
|
||||
import { isValidPath } from './utils';
|
||||
import { Visitor } from '../types';
|
||||
import { addMethod, addProperty, addSubComponent, createComponentNode } from '../nodeFactory';
|
||||
import { isValidPath } from '../utils';
|
||||
import { type NodePath } from '@babel/core';
|
||||
import { COMPONENT } from '../constants';
|
||||
import { analyzeFnComp } from '.';
|
||||
import { getDependenciesFromNode } from './reactive/getDependencies';
|
||||
import { COMPONENT } from '../../constants';
|
||||
import { analyzeFnComp } from '../index';
|
||||
import { getDependenciesFromNode } from '@openinula/reactivity-parser';
|
||||
import { types as t } from '@openinula/babel-api';
|
||||
import { reactivityFuncNames } from '../../const';
|
||||
|
||||
/**
|
||||
* collect all properties and methods from the node
|
||||
|
@ -43,7 +44,7 @@ export function variablesAnalyze(): Visitor {
|
|||
} else if (id.isIdentifier()) {
|
||||
// --- properties: the state / computed / plain properties / methods ---
|
||||
const init = declaration.get('init');
|
||||
let depBits = 0;
|
||||
let dependency;
|
||||
if (isValidPath(init)) {
|
||||
// handle the method
|
||||
if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
|
||||
|
@ -68,9 +69,9 @@ export function variablesAnalyze(): Visitor {
|
|||
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.
|
||||
*/
|
||||
|
||||
import { Visitor } from './types';
|
||||
import { Visitor } from '../types';
|
||||
import { type NodePath } from '@babel/core';
|
||||
import { parseView as parseJSX } from '@openinula/jsx-view-parser';
|
||||
import { types as t, getBabelApi } from '@openinula/babel-api';
|
||||
import { parseReactivity } from '@openinula/reactivity-parser';
|
||||
import { reactivityFuncNames } from '../const';
|
||||
import { setViewChild } from './nodeFactory';
|
||||
import { reactivityFuncNames } from '../../const';
|
||||
import { setViewChild } from '../nodeFactory';
|
||||
|
||||
/**
|
||||
* Analyze the watch in the function component
|
||||
|
@ -34,15 +34,14 @@ export function viewAnalyze(): Visitor {
|
|||
htmlTags,
|
||||
parseTemplate: false,
|
||||
});
|
||||
// @ts-expect-error TODO: FIX TYPE
|
||||
const [viewParticles, usedPropertySet, usedBit] = parseReactivity(viewUnits, {
|
||||
|
||||
const [viewParticles, usedBit] = parseReactivity(viewUnits, {
|
||||
babelApi: getBabelApi(),
|
||||
availableProperties: current.availableVariables,
|
||||
depMaskMap: current._reactiveBitMap,
|
||||
reactivityFuncNames,
|
||||
});
|
||||
|
||||
setViewChild(current, viewParticles, usedPropertySet, usedBit);
|
||||
setViewChild(current, viewParticles, usedBit);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -1,14 +1,13 @@
|
|||
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 { variablesAnalyze } from './variablesAnalyze';
|
||||
import { functionalMacroAnalyze } from './functionalMacroAnalyze';
|
||||
import { variablesAnalyze } from './Analyzers/variablesAnalyze';
|
||||
import { functionalMacroAnalyze } from './Analyzers/functionalMacroAnalyze';
|
||||
import { getFnBodyPath } from '../utils';
|
||||
import { viewAnalyze } from './viewAnalyze';
|
||||
import { viewAnalyze } from './Analyzers/viewAnalyze';
|
||||
import { WILL_MOUNT } from '../constants';
|
||||
import { types as t } from '@openinula/babel-api';
|
||||
import { ViewParticle } from '@openinula/reactivity-parser';
|
||||
import { pruneComponentUnusedBit } from './pruneComponentUnusedBit';
|
||||
import { pruneUnusedBit } from './pruneUnusedBit';
|
||||
|
||||
const builtinAnalyzers = [variablesAnalyze, functionalMacroAnalyze, viewAnalyze];
|
||||
|
||||
|
@ -98,7 +97,7 @@ export function analyze(
|
|||
const root = createComponentNode(fnName, path);
|
||||
analyzeFnComp(path, root, { analyzers, htmlTags: options.htmlTags });
|
||||
|
||||
pruneComponentUnusedBit(root);
|
||||
pruneUnusedBit(root);
|
||||
|
||||
return root;
|
||||
}
|
|
@ -14,9 +14,8 @@
|
|||
*/
|
||||
|
||||
import { NodePath, type types as t } from '@babel/core';
|
||||
import type { ComponentNode, FunctionalExpression, LifeCycle, ReactiveVariable, Bitmap } from './types';
|
||||
import { PropType } from '../constants';
|
||||
import { ViewParticle } from '@openinula/reactivity-parser';
|
||||
import type { ComponentNode, FunctionalExpression, LifeCycle, ReactiveVariable, Dependency } from './types';
|
||||
import { Bitmap, ViewParticle } from '@openinula/reactivity-parser';
|
||||
|
||||
export function createComponentNode(
|
||||
name: string,
|
||||
|
@ -30,7 +29,6 @@ export function createComponentNode(
|
|||
children: undefined,
|
||||
variables: [],
|
||||
usedBit: 0,
|
||||
usedPropertySet: parent ? new Set(parent.usedPropertySet) : new Set<string>(),
|
||||
_reactiveBitMap: parent ? new Map<string, number>(parent._reactiveBitMap) : new Map<string, number>(),
|
||||
lifecycle: {},
|
||||
parent,
|
||||
|
@ -48,17 +46,30 @@ export function createComponentNode(
|
|||
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
|
||||
const idx = comp.availableVariables.length;
|
||||
const bit = 1 << idx;
|
||||
const bitmap = depBits ? depBits | bit : bit;
|
||||
const fullDepBits = dependency?.fullDepMask;
|
||||
const bitmap = fullDepBits ? fullDepBits | bit : bit;
|
||||
|
||||
if (depBits) {
|
||||
comp.usedBit |= depBits;
|
||||
if (fullDepBits) {
|
||||
comp.usedBit |= fullDepBits;
|
||||
}
|
||||
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) {
|
||||
|
@ -81,21 +92,21 @@ export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: t
|
|||
export function addWatch(
|
||||
comp: ComponentNode,
|
||||
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>,
|
||||
deps: Set<string>,
|
||||
usedBit: Bitmap
|
||||
dependency: Dependency
|
||||
) {
|
||||
// if watch not exist, create a new one
|
||||
if (!comp.watch) {
|
||||
comp.watch = [];
|
||||
}
|
||||
comp.usedPropertySet = new Set([...comp.usedPropertySet, ...deps]);
|
||||
comp.usedBit |= usedBit;
|
||||
comp.watch.push({ callback });
|
||||
comp.usedBit |= dependency.fullDepMask;
|
||||
comp.watch.push({
|
||||
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
|
||||
comp.usedPropertySet = usedPropertySet;
|
||||
comp.usedBit |= usedBit;
|
||||
comp.children = view;
|
||||
}
|
|
@ -13,56 +13,62 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { Bitmap, ComponentNode } from './types';
|
||||
import { ViewParticle } from '@openinula/reactivity-parser';
|
||||
import { ComponentNode } from './types';
|
||||
import { Bitmap, ViewParticle } from '@openinula/reactivity-parser';
|
||||
|
||||
/**
|
||||
* To prune the bitmap of unused properties
|
||||
* Here the depMask will be defined by prune the unused bit in fullDepMask
|
||||
* etc.:
|
||||
* ```js
|
||||
* let a = 1; // 0b001
|
||||
* let b = 2; // 0b010 b is not used*, and should be 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
|
||||
// To store the bitmap of the properties
|
||||
const bitMap = new Map<string, number>();
|
||||
const bitPositionToRemove: number[] = [];
|
||||
const bitPositionToRemove: number[] = [...bitPositionToRemoveInParent];
|
||||
comp.variables.forEach(v => {
|
||||
if (v.type === 'reactive') {
|
||||
// get the origin bit, computed should keep the highest bit, etc. 0b0111 -> 0b0100
|
||||
const originBit = keepHighestBit(v.depMask);
|
||||
if ((comp.usedBit & originBit) !== 0) {
|
||||
v.bit = 1 << index;
|
||||
const originBit = keepHighestBit(v._fullBits);
|
||||
|
||||
if (comp.usedBit & originBit) {
|
||||
v.bit = 1 << (index - bitPositionToRemove.length - 1);
|
||||
bitMap.set(v.name, v.bit);
|
||||
if (v.isComputed) {
|
||||
v.depMask = pruneBitmap(v.depMask, bitPositionToRemove);
|
||||
if (v.dependency) {
|
||||
// 去掉最高位
|
||||
v.dependency.depMask = getDepMask(v.dependency.depBitmaps, bitPositionToRemove);
|
||||
}
|
||||
} else {
|
||||
bitPositionToRemove.push(index);
|
||||
}
|
||||
index++;
|
||||
} else if (v.type === 'subComp') {
|
||||
pruneComponentUnusedBit(v, index);
|
||||
pruneUnusedBit(v, index, bitPositionToRemove);
|
||||
}
|
||||
});
|
||||
|
||||
comp.watch?.forEach(watch => {
|
||||
if (!watch.depMask) {
|
||||
const dependency = watch.dependency;
|
||||
if (!dependency) {
|
||||
return;
|
||||
}
|
||||
watch.depMask = pruneBitmap(watch.depMask, bitPositionToRemove);
|
||||
dependency.depMask = getDepMask(dependency.depBitmaps, bitPositionToRemove);
|
||||
});
|
||||
|
||||
// handle children
|
||||
if (comp.children) {
|
||||
comp.children.forEach(child => {
|
||||
if (child.type === 'comp') {
|
||||
pruneComponentUnusedBit(child as ComponentNode<'comp'>, index);
|
||||
pruneUnusedBit(child as ComponentNode<'comp'>, index, bitPositionToRemove);
|
||||
} else {
|
||||
pruneViewParticleUnusedBit(child as ViewParticle, bitPositionToRemove);
|
||||
}
|
||||
|
@ -72,18 +78,25 @@ export function pruneComponentUnusedBit(comp: ComponentNode<'comp'> | ComponentN
|
|||
|
||||
function pruneBitmap(depMask: Bitmap, bitPositionToRemove: number[]) {
|
||||
// turn the bitmap to binary string
|
||||
const binary = depMask.toString(2);
|
||||
// remove the bit
|
||||
binary
|
||||
.split('')
|
||||
.reverse()
|
||||
.filter((bit, index) => {
|
||||
return !bitPositionToRemove.includes(index);
|
||||
})
|
||||
.reverse()
|
||||
.join('');
|
||||
const binaryStr = depMask.toString(2);
|
||||
const length = binaryStr.length;
|
||||
// iterate the binaryStr to keep the bit that is not in the bitPositionToRemove
|
||||
let result = '';
|
||||
for (let i = length; i > 0; i--) {
|
||||
if (!bitPositionToRemove.includes(i)) {
|
||||
result = result + binaryStr[length - i];
|
||||
}
|
||||
}
|
||||
|
||||
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[]) {
|
||||
|
@ -93,34 +106,31 @@ function pruneViewParticleUnusedBit(particle: ViewParticle, bitPositionToRemove:
|
|||
const node = stack.pop()! as ViewParticle;
|
||||
if (node.type === 'template') {
|
||||
node.props.forEach(prop => {
|
||||
prop.depMask = pruneBitmap(prop.depMask, bitPositionToRemove);
|
||||
prop.depMask = getDepMask(prop.depBitmaps, bitPositionToRemove);
|
||||
});
|
||||
stack.push(node.template);
|
||||
} else if (node.type === 'html') {
|
||||
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);
|
||||
} 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') {
|
||||
node.array.depMask = pruneBitmap(node.array.depMask, bitPositionToRemove);
|
||||
node.array.depMask = getDepMask(node.array.depBitmaps, bitPositionToRemove);
|
||||
stack.push(...node.children);
|
||||
} else if (node.type === 'if') {
|
||||
node.branches.forEach(branch => {
|
||||
branch.condition.depMask = pruneBitmap(branch.condition.depMask, bitPositionToRemove);
|
||||
branch.condition.depMask = getDepMask(branch.condition.depBitmaps, bitPositionToRemove);
|
||||
stack.push(...branch.children);
|
||||
});
|
||||
} else if (node.type === 'env') {
|
||||
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);
|
||||
} else if (node.type === 'exp') {
|
||||
node.content.depMask = pruneBitmap(node.content.depMask, bitPositionToRemove);
|
||||
for (const key in node.props) {
|
||||
node.props[key].depMask = pruneBitmap(node.props[key].depMask, bitPositionToRemove);
|
||||
}
|
||||
node.content.depMask = getDepMask(node.content.depBitmaps, bitPositionToRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -135,20 +145,3 @@ function keepHighestBit(bitmap: number) {
|
|||
// 使用按位与运算符只保留最高位
|
||||
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 { 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 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;
|
||||
|
||||
interface BaseVariable<V> {
|
||||
|
@ -32,16 +43,14 @@ export interface ReactiveVariable extends BaseVariable<t.Expression | null> {
|
|||
level: number;
|
||||
bit?: Bitmap;
|
||||
/**
|
||||
* indicate the dependency of the variable | the index of the reactive variable
|
||||
* Contains the bit of all dependencies graph
|
||||
* i.e.
|
||||
* let name = 'John'; // name's bitmap is 0x0001
|
||||
* let age = 18; // age's bitmap is 0x0010
|
||||
* let greeting = `Hello, ${name}`; // greeting's bitmap is 0x0101
|
||||
* let name = 'John'; // name's _fullBits is 0x0001
|
||||
* let age = 18; // age's _fullBits is 0x0010
|
||||
* let greeting = `Hello, ${name}`; // greeting's _fullBits is 0x0101
|
||||
*/
|
||||
depMask: Bitmap;
|
||||
// need a flag for computed to gen a getter
|
||||
// watch is a static computed
|
||||
isComputed: boolean;
|
||||
_fullBits: Bitmap;
|
||||
dependency: Dependency | null;
|
||||
}
|
||||
|
||||
export interface MethodVariable extends BaseVariable<FunctionalExpression> {
|
||||
|
@ -67,10 +76,6 @@ export interface ComponentNode<Type = 'comp'> {
|
|||
level: number;
|
||||
// The variables defined in the component
|
||||
variables: Variable[];
|
||||
/**
|
||||
* The used properties in the component
|
||||
*/
|
||||
usedPropertySet: Set<string>;
|
||||
usedBit: Bitmap;
|
||||
/**
|
||||
* The map to find the reactive bitmap by name
|
||||
|
@ -95,7 +100,7 @@ export interface ComponentNode<Type = 'comp'> {
|
|||
* The watch fn in the component
|
||||
*/
|
||||
watch?: {
|
||||
depMask?: Bitmap;
|
||||
dependency: Dependency | null;
|
||||
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 { 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 {
|
||||
|
@ -13,25 +11,9 @@ export default function (_: ConfigAPI, options: DLightOption): TransformOptions
|
|||
syntaxJSX.default ?? syntaxJSX,
|
||||
[syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }],
|
||||
[syntaxDecorators.default ?? syntaxDecorators, { legacy: true }],
|
||||
fn2Class,
|
||||
[inulaNext, 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;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import type babel from '@babel/core';
|
|||
import { NodePath, type PluginObj, type types as t } from '@babel/core';
|
||||
import { type DLightOption } from './types';
|
||||
import { defaultAttributeMap, defaultHTMLTags } from './const';
|
||||
import { analyze } from './analyzer';
|
||||
import { analyze } from './analyze';
|
||||
import { COMPONENT } from './constants';
|
||||
import { extractFnFromMacro, isCompPath } from './utils';
|
||||
import { register } from '@openinula/babel-api';
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
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';
|
||||
|
||||
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]);
|
||||
|
|
|
@ -15,8 +15,9 @@
|
|||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { genCode, mockAnalyze } from '../mock';
|
||||
import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
|
||||
import { ReactiveVariable, SubCompVariable } from '../../src/analyzer/types';
|
||||
import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze';
|
||||
import { ReactiveVariable, SubCompVariable } from '../../src/analyze/types';
|
||||
import { findReactiveVarByName } from './utils';
|
||||
|
||||
const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze]);
|
||||
|
||||
|
@ -37,17 +38,18 @@ describe('analyze properties', () => {
|
|||
Component(() => {
|
||||
let foo = 1;
|
||||
let bar = foo;
|
||||
let _ = bar; // use bar to avoid pruning
|
||||
})
|
||||
`);
|
||||
expect(root.variables.length).toBe(2);
|
||||
const fooVar = root.variables[0] as ReactiveVariable;
|
||||
expect(fooVar.isComputed).toBe(false);
|
||||
const fooVar = findReactiveVarByName(root, 'foo');
|
||||
expect(!!fooVar.dependency).toBe(false);
|
||||
expect(genCode(fooVar.value)).toBe('1');
|
||||
|
||||
const barVar = root.variables[1] as ReactiveVariable;
|
||||
expect(barVar.isComputed).toBe(true);
|
||||
const barVar = findReactiveVarByName(root, 'bar');
|
||||
expect(!!barVar.dependency).toBe(true);
|
||||
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', () => {
|
||||
|
@ -57,18 +59,18 @@ describe('analyze properties', () => {
|
|||
let a = 1;
|
||||
let b = 0;
|
||||
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;
|
||||
expect(barVar.isComputed).toBe(true);
|
||||
expect(!!barVar.dependency).toBe(true);
|
||||
expect(genCode(barVar.value)).toMatchInlineSnapshot(`
|
||||
"{
|
||||
foo: foo ? a : b
|
||||
}"
|
||||
`);
|
||||
expect(barVar.depMask).toEqual(0b1111);
|
||||
expect(barVar.dependency!.depMask).toEqual(0b0111);
|
||||
});
|
||||
|
||||
// TODO:MOVE TO PROPS PLUGIN TEST
|
||||
|
@ -81,7 +83,7 @@ describe('analyze properties', () => {
|
|||
expect(root.variables.length).toBe(1);
|
||||
|
||||
const barVar = root.variables[0] as ReactiveVariable;
|
||||
expect(barVar.isComputed).toBe(true);
|
||||
expect(!!barVar.dependency).toBe(true);
|
||||
});
|
||||
|
||||
// TODO:MOVE TO PROPS PLUGIN TEST
|
||||
|
@ -93,7 +95,7 @@ describe('analyze properties', () => {
|
|||
`);
|
||||
expect(root.variables.length).toBe(1);
|
||||
const barVar = root.variables[0] as ReactiveVariable;
|
||||
expect(barVar.isComputed).toBe(true);
|
||||
expect(!!barVar.dependency).toBe(true);
|
||||
// @ts-expect-error ignore ts here
|
||||
expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] });
|
||||
});
|
||||
|
@ -103,12 +105,12 @@ describe('analyze properties', () => {
|
|||
const cond = true
|
||||
Component(() => {
|
||||
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;
|
||||
expect(barVar.isComputed).toBe(false);
|
||||
expect(barVar.depMask).toEqual(0b1);
|
||||
expect(!!barVar.dependency).toBe(false);
|
||||
expect(barVar.bit).toEqual(0b1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -119,16 +121,15 @@ describe('analyze properties', () => {
|
|||
let foo = 1;
|
||||
const Sub = Component(() => {
|
||||
let bar = foo;
|
||||
let _ = bar; // use bar to avoid pruning
|
||||
});
|
||||
})
|
||||
`);
|
||||
expect(root.variables.length).toBe(2);
|
||||
expect(root.availableVariables[0].depMask).toEqual(0b1);
|
||||
expect((root.variables[1] as SubCompVariable).ownAvailableVariables[0].depMask).toBe(0b11);
|
||||
expect((root.variables[1] as SubCompVariable).ownAvailableVariables[0].dependency!.depMask).toBe(0b1);
|
||||
});
|
||||
|
||||
it('should analyze dependency in parent', () => {
|
||||
const root = analyze(`
|
||||
const root = analyze(/*jsx*/ `
|
||||
Component(() => {
|
||||
let lastName;
|
||||
let parentFirstName = 'sheldon';
|
||||
|
@ -137,19 +138,20 @@ describe('analyze properties', () => {
|
|||
let middleName = parentName
|
||||
const name = 'shelly'+ middleName + lastName;
|
||||
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;
|
||||
// Son > middleName
|
||||
expect(sonNode.ownAvailableVariables[0].depMask).toBe(0b1111);
|
||||
expect(findReactiveVarByName(sonNode, 'middleName').dependency!.depMask).toBe(0b100);
|
||||
// Son > name
|
||||
expect(sonNode.ownAvailableVariables[1].depMask).toBe(0b11111);
|
||||
expect(findReactiveVarByName(sonNode, 'name').dependency!.depMask).toBe(0b1001);
|
||||
const grandSonNode = sonNode.variables[2] as SubCompVariable;
|
||||
// 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.
|
||||
*/
|
||||
|
||||
import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
|
||||
import { ComponentNode } from '../../src/analyzer/types';
|
||||
import { viewAnalyze } from '../../src/analyzer/viewAnalyze';
|
||||
import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze';
|
||||
import { genCode, mockAnalyze } from '../mock';
|
||||
import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze';
|
||||
import { viewAnalyze } from '../../src/analyze/Analyzers/viewAnalyze';
|
||||
import { functionalMacroAnalyze } from '../../src/analyze/Analyzers/functionalMacroAnalyze';
|
||||
import { mockAnalyze } from '../mock';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { findReactiveVarByName, findSubCompByName } from './utils';
|
||||
|
||||
const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze, functionalMacroAnalyze]);
|
||||
describe('prune unused bit', () => {
|
||||
it('should work', () => {
|
||||
const root = analyze(/*js*/ `
|
||||
Component(({}) => {
|
||||
let name;
|
||||
let className; // unused
|
||||
let className1; // unused
|
||||
let className2; // unused
|
||||
let count = name; // 1
|
||||
let doubleCount = count * 2; // 2
|
||||
let unused0;
|
||||
let name; // 0b1
|
||||
let unused;
|
||||
let unused1;
|
||||
let unused2;
|
||||
let count = name; // 0b10
|
||||
let doubleCount = count * 2; // 0b100
|
||||
const Input = Component(() => {
|
||||
let count3 = 1;
|
||||
let count2 = 1;
|
||||
let count = 1;
|
||||
return <input>{count}{doubleCount}</input>;
|
||||
let count = 1; // 0b1000
|
||||
const db = count * 2; // 0b10000
|
||||
return <input>{count}{db * count}</input>;
|
||||
});
|
||||
return <div className={count}>{doubleCount}</div>;
|
||||
});
|
||||
`);
|
||||
const div = root.children![0] as any;
|
||||
expect(div.children[0].content.depMask).toEqual(0b111);
|
||||
expect(div.props.className.depMask).toEqual(0b11);
|
||||
// test computed
|
||||
const countVar = findReactiveVarByName(root, 'count');
|
||||
expect(countVar.bit).toEqual(0b10);
|
||||
expect(countVar.dependency!.depMask).toEqual(0b1);
|
||||
|
||||
// @ts-expect-error ignore ts here
|
||||
const InputCompNode = root.variables[4] as ComponentNode;
|
||||
// it's the {count}
|
||||
expect(inputFirstExp.content.depMask).toEqual(0b10000);
|
||||
// it's the {doubleCount}
|
||||
expect(inputSecondExp.content.depMask).toEqual(0b1101);
|
||||
// test view
|
||||
const div = root.children![0] as any;
|
||||
expect(div.children[0].content.depMask).toEqual(0b100);
|
||||
expect(div.props.className.depMask).toEqual(0b10);
|
||||
|
||||
// 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 { ComponentNode } from '../../src/analyzer/types';
|
||||
import { viewAnalyze } from '../../src/analyzer/viewAnalyze';
|
||||
import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze';
|
||||
import { ComponentNode } from '../../src/analyze/types';
|
||||
import { viewAnalyze } from '../../src/analyze/Analyzers/viewAnalyze';
|
||||
import { genCode, mockAnalyze } from '../mock';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { findSubCompByName } from './utils';
|
||||
|
||||
const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze]);
|
||||
describe('viewAnalyze', () => {
|
||||
|
@ -22,27 +23,22 @@ describe('viewAnalyze', () => {
|
|||
});
|
||||
`);
|
||||
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(div.props.className.depMask).toEqual(0b111);
|
||||
expect(div.props.className.depMask).toEqual(0b110);
|
||||
expect(genCode(div.props.className.value)).toMatchInlineSnapshot('"className + count"');
|
||||
|
||||
const InputCompNode = findSubCompByName(root, 'Input');
|
||||
// @ts-expect-error ignore ts here
|
||||
const InputCompNode = root.variables[5] as ComponentNode;
|
||||
expect(InputCompNode.usedPropertySet).toMatchInlineSnapshot(`
|
||||
Set {
|
||||
"count",
|
||||
"doubleCount",
|
||||
}
|
||||
`);
|
||||
// it's the {count}
|
||||
const inputFirstExp = InputCompNode.children![0].children[0];
|
||||
expect(inputFirstExp.content.depMask).toEqual(0b100000);
|
||||
expect(genCode(inputFirstExp.content.dependenciesNode)).toMatchInlineSnapshot('"[count]"');
|
||||
|
||||
// @ts-expect-error ignore ts here
|
||||
// it's the {doubleCount}
|
||||
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]"');
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze';
|
||||
import { functionalMacroAnalyze } from '../../src/analyze/Analyzers/functionalMacroAnalyze';
|
||||
import { genCode, mockAnalyze } from '../mock';
|
||||
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]);
|
||||
|
||||
|
@ -25,10 +25,10 @@ describe('watchAnalyze', () => {
|
|||
console.log(a, b);
|
||||
}"
|
||||
`);
|
||||
if (!root.watch[0].depMask) {
|
||||
if (!root.watch[0].dependency) {
|
||||
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', () => {
|
||||
|
@ -50,9 +50,9 @@ describe('watchAnalyze', () => {
|
|||
// watch expression
|
||||
}"
|
||||
`);
|
||||
if (!root.watch[0].depMask) {
|
||||
if (!root.watch[0].dependency) {
|
||||
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.
|
||||
*/
|
||||
|
||||
import { Analyzer, ComponentNode } from '../src/analyzer/types';
|
||||
import { Analyzer, ComponentNode } from '../src/analyze/types';
|
||||
import { type PluginObj, transform as transformWithBabel } from '@babel/core';
|
||||
import syntaxJSX from '@babel/plugin-syntax-jsx';
|
||||
import { analyze } from '../src/analyzer';
|
||||
import { analyze } from '../src/analyze';
|
||||
import generate from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
import { register } from '@openinula/babel-api';
|
||||
|
|
|
@ -2,12 +2,19 @@
|
|||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"ts-node": {
|
||||
"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
|
||||
* 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
|
||||
* @returns mutable particles
|
||||
*/
|
||||
|
@ -435,7 +435,7 @@ export class ViewParser {
|
|||
const templateProps: TemplateProp[] = [];
|
||||
const generateVariableProp = (unit: HTMLUnit, path: number[]) => {
|
||||
// ---- 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 &&
|
||||
Object.entries(unit.props)
|
||||
.filter(([, prop]) => !this.isStaticProp(prop))
|
||||
|
@ -489,7 +489,7 @@ export class ViewParser {
|
|||
|
||||
/**
|
||||
* @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
|
||||
* @returns filtered props
|
||||
*/
|
||||
|
@ -539,7 +539,8 @@ export class ViewParser {
|
|||
// ---- Get array
|
||||
const arrayContainer = this.findProp(node, 'array');
|
||||
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;
|
||||
if (this.t.isJSXEmptyExpression(array)) throw new Error('Expected [array] expression not empty');
|
||||
|
||||
|
@ -555,17 +556,18 @@ export class ViewParser {
|
|||
// ---- Get Item
|
||||
const itemProp = this.findProp(node, 'item');
|
||||
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;
|
||||
if (this.t.isJSXEmptyExpression(item)) throw new Error('Expected [item] expression not empty');
|
||||
// ---- ObjectExpression to ObjectPattern / ArrayExpression to ArrayPattern
|
||||
this.traverse(this.wrapWithFile(item), {
|
||||
ObjectExpression: (path) => {
|
||||
ObjectExpression: path => {
|
||||
path.node.type = 'ObjectPattern' as any;
|
||||
},
|
||||
ArrayExpression: (path) => {
|
||||
ArrayExpression: path => {
|
||||
path.node.type = 'ArrayPattern' as any;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ---- Get children
|
||||
|
@ -576,7 +578,7 @@ export class ViewParser {
|
|||
key,
|
||||
item: item as t.LVal,
|
||||
array,
|
||||
children: this.parseView(children)
|
||||
children: this.parseView(children),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
"dependencies": {
|
||||
"@openinula/error-handler": "workspace:*",
|
||||
"@openinula/jsx-view-parser": "workspace:*",
|
||||
"@openinula/babel-api": "workspace:*",
|
||||
"@openinula/view-parser": "workspace:*"
|
||||
},
|
||||
"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
|
||||
* @returns [viewParticles, usedProperties]
|
||||
*/
|
||||
export function parseReactivity(
|
||||
viewUnits: ViewUnit[],
|
||||
config: ReactivityParserConfig
|
||||
): [ViewParticle[], Set<string>, number] {
|
||||
export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserConfig): [ViewParticle[], number] {
|
||||
// ---- ReactivityParser only accepts one view unit at a time,
|
||||
// so we loop through the view units and get all the used properties
|
||||
const usedProperties = new Set<string>();
|
||||
let usedBit = 0;
|
||||
const dlParticles = viewUnits.map(viewUnit => {
|
||||
const parser = new ReactivityParser(config);
|
||||
const dlParticle = parser.parse(viewUnit);
|
||||
parser.usedProperties.forEach(usedProperties.add.bind(usedProperties));
|
||||
usedBit |= parser.usedBit;
|
||||
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 {
|
||||
type TemplateProp,
|
||||
type ReactivityParserConfig,
|
||||
type MutableParticle,
|
||||
type ViewParticle,
|
||||
type TemplateParticle,
|
||||
type TextParticle,
|
||||
type HTMLParticle,
|
||||
type DependencyProp,
|
||||
type ExpParticle,
|
||||
type CompParticle,
|
||||
type ForParticle,
|
||||
type IfParticle,
|
||||
type EnvParticle,
|
||||
type DependencyProp,
|
||||
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';
|
||||
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 {
|
||||
type TextUnit,
|
||||
type HTMLUnit,
|
||||
type ViewUnit,
|
||||
type CompUnit,
|
||||
type UnitProp,
|
||||
type ForUnit,
|
||||
type IfUnit,
|
||||
type EnvUnit,
|
||||
type ExpUnit,
|
||||
type ForUnit,
|
||||
type HTMLUnit,
|
||||
type IfUnit,
|
||||
type TextUnit,
|
||||
type UnitProp,
|
||||
type ViewUnit,
|
||||
} from '@openinula/jsx-view-parser';
|
||||
import { DLError } from './error';
|
||||
import { getDependenciesFromNode } from './getDependencies';
|
||||
|
||||
export class ReactivityParser {
|
||||
private readonly config: ReactivityParserConfig;
|
||||
|
||||
private readonly t: typeof t;
|
||||
private readonly traverse: typeof traverse;
|
||||
private readonly availableProperties: string[];
|
||||
private readonly depMaskMap: DepMaskMap;
|
||||
private readonly identifierDepMap: Record<string, string[]>;
|
||||
private readonly reactivityFuncNames;
|
||||
|
||||
private readonly escapeNamings = ['escape', '$'];
|
||||
private static readonly customHTMLProps = [
|
||||
'didUpdate',
|
||||
'willMount',
|
||||
|
@ -66,7 +64,6 @@ export class ReactivityParser {
|
|||
this.config = config;
|
||||
this.t = config.babelApi.types;
|
||||
this.traverse = config.babelApi.traverse;
|
||||
this.availableProperties = config.availableProperties;
|
||||
this.depMaskMap = config.depMaskMap;
|
||||
this.reactivityFuncNames = config.reactivityFuncNames ?? [];
|
||||
}
|
||||
|
@ -162,7 +159,7 @@ export class ReactivityParser {
|
|||
/**
|
||||
* @brief Collect all the mutable nodes in a static HTMLUnit
|
||||
* 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
|
||||
* @returns mutable particles
|
||||
*/
|
||||
|
@ -204,7 +201,7 @@ export class ReactivityParser {
|
|||
const templateProps: TemplateProp[] = [];
|
||||
const generateVariableProp = (unit: HTMLUnit, path: number[]) => {
|
||||
// ---- 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)
|
||||
.filter(([, prop]) => !this.isStaticProp(prop))
|
||||
.forEach(([key, prop]) => {
|
||||
|
@ -235,6 +232,7 @@ export class ReactivityParser {
|
|||
value: child.content,
|
||||
depMask: 0,
|
||||
dependenciesNode: this.t.arrayExpression([]),
|
||||
depBitmaps: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -341,6 +339,7 @@ export class ReactivityParser {
|
|||
item: forUnit.item,
|
||||
array: {
|
||||
value: forUnit.array,
|
||||
depBitmaps: [],
|
||||
depMask: depMask,
|
||||
dependenciesNode,
|
||||
},
|
||||
|
@ -393,14 +392,10 @@ export class ReactivityParser {
|
|||
* @returns ExpParticle
|
||||
*/
|
||||
private parseExp(expUnit: ExpUnit): ExpParticle {
|
||||
const expParticle: ExpParticle = {
|
||||
return {
|
||||
type: 'exp',
|
||||
content: this.generateDependencyProp(expUnit.content),
|
||||
props: Object.fromEntries(
|
||||
Object.entries(expUnit.props).map(([key, prop]) => [key, this.generateDependencyProp(prop)])
|
||||
),
|
||||
};
|
||||
return expParticle;
|
||||
}
|
||||
|
||||
// ---- Dependencies ----
|
||||
|
@ -410,14 +405,13 @@ export class ReactivityParser {
|
|||
* @returns DependencyProp
|
||||
*/
|
||||
private generateDependencyProp(prop: UnitProp): DependencyProp {
|
||||
const dependencyProp: DependencyProp = {
|
||||
return {
|
||||
value: prop.value,
|
||||
...this.getDependencies(prop.value),
|
||||
viewPropMap: Object.fromEntries(
|
||||
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
|
||||
* @returns dependency index array
|
||||
*/
|
||||
private getDependencies(node: t.Expression | t.Statement): {
|
||||
depMask: number;
|
||||
dependenciesNode: t.ArrayExpression;
|
||||
} {
|
||||
private getDependencies(node: t.Expression | t.Statement) {
|
||||
if (this.t.isFunctionExpression(node) || this.t.isArrowFunctionExpression(node)) {
|
||||
return {
|
||||
depMask: 0,
|
||||
depBitmaps: [],
|
||||
dependenciesNode: this.t.arrayExpression([]),
|
||||
};
|
||||
}
|
||||
// ---- Both id and prop deps need to be calculated because
|
||||
// id is for snippet update, prop is normal update
|
||||
// in a snippet, the depsNode should be both id and prop
|
||||
const [deps, propertyDepNodes] = this.getPropertyDependencies(node);
|
||||
|
||||
const depNodes = [...propertyDepNodes] as t.Expression[];
|
||||
|
||||
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));
|
||||
const dependency = getDependenciesFromNode(node, this.depMaskMap, this.reactivityFuncNames);
|
||||
this.usedBit |= dependency.fullDepMask;
|
||||
return dependency;
|
||||
}
|
||||
|
||||
// ---- Utils ----
|
||||
|
@ -626,7 +491,7 @@ export class ReactivityParser {
|
|||
|
||||
/**
|
||||
* @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
|
||||
* @returns filtered props
|
||||
*/
|
||||
|
@ -750,112 +615,4 @@ export class ReactivityParser {
|
|||
|
||||
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> {
|
||||
value: T;
|
||||
depMask: number; // -> bit
|
||||
depMask?: number; // -> bit
|
||||
depBitmaps: number[];
|
||||
dependenciesNode: t.ArrayExpression;
|
||||
}
|
||||
|
||||
export interface DependencyProp {
|
||||
value: t.Expression;
|
||||
viewPropMap: Record<string, ViewParticle[]>;
|
||||
depMask: number;
|
||||
depMask?: number;
|
||||
depBitmaps: number[];
|
||||
dependenciesNode: t.ArrayExpression;
|
||||
}
|
||||
|
||||
|
@ -19,7 +21,8 @@ export interface TemplateProp {
|
|||
key: string;
|
||||
path: number[];
|
||||
value: t.Expression;
|
||||
depMask: number;
|
||||
depMask?: number;
|
||||
depBitmaps: number[];
|
||||
dependenciesNode: t.ArrayExpression;
|
||||
}
|
||||
|
||||
|
@ -78,7 +81,6 @@ export interface EnvParticle {
|
|||
export interface ExpParticle {
|
||||
type: 'exp';
|
||||
content: DependencyProp;
|
||||
props: Record<string, DependencyProp>;
|
||||
}
|
||||
|
||||
export type ViewParticle =
|
||||
|
@ -93,8 +95,6 @@ export type ViewParticle =
|
|||
|
||||
export interface ReactivityParserConfig {
|
||||
babelApi: typeof Babel;
|
||||
availableProperties: string[];
|
||||
availableIdentifiers?: string[];
|
||||
depMaskMap: DepMaskMap;
|
||||
identifierDepMap?: Record<string, Bitmap>;
|
||||
dependencyParseType?: 'property' | 'identifier';
|
||||
|
@ -103,5 +103,5 @@ export interface ReactivityParserConfig {
|
|||
}
|
||||
|
||||
// TODO: unify with the types in babel-inula-next-core
|
||||
type Bitmap = number;
|
||||
export type Bitmap = number;
|
||||
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