refactor(parser): prune unused bit

This commit is contained in:
Hoikan 2024-05-22 17:13:43 +08:00
parent 5fc41b8e5e
commit 4a877cdc34
93 changed files with 507 additions and 8241 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: ['*'],
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
}

View File

@ -1,7 +0,0 @@
# @openinula/class-transformer
## 0.0.2
### Patch Changes
- feat: add lifecycles and watch

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
export interface Option {
files?: string | string[];
excludeFiles?: string | string[];
htmlTags?: string[];
parseTemplate?: boolean;
attributeMap?: Record<string, string>;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
}

23
test.md Normal file
View File

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