diff --git a/packages/transpiler/babel-inula-next-core/package.json b/packages/transpiler/babel-inula-next-core/package.json index 12124df1..a1c1ca62 100644 --- a/packages/transpiler/babel-inula-next-core/package.json +++ b/packages/transpiler/babel-inula-next-core/package.json @@ -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", diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/functionalMacroAnalyze.ts similarity index 77% rename from packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts rename to packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/functionalMacroAnalyze.ts index a9e07350..41168174 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/functionalMacroAnalyze.ts @@ -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; } } diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/hookAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/hookAnalyze.ts similarity index 100% rename from packages/transpiler/babel-inula-next-core/src/analyzer/hookAnalyze.ts rename to packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/hookAnalyze.ts diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/variablesAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/variablesAnalyze.ts similarity index 87% rename from packages/transpiler/babel-inula-next-core/src/analyzer/variablesAnalyze.ts rename to packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/variablesAnalyze.ts index f3639300..f3a52728 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/variablesAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/variablesAnalyze.ts @@ -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); } }); }, diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/viewAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/viewAnalyze.ts similarity index 78% rename from packages/transpiler/babel-inula-next-core/src/analyzer/viewAnalyze.ts rename to packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/viewAnalyze.ts index be6edb79..d9fbea9d 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/viewAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/viewAnalyze.ts @@ -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); } }, }; diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/index.ts b/packages/transpiler/babel-inula-next-core/src/analyze/index.ts similarity index 87% rename from packages/transpiler/babel-inula-next-core/src/analyzer/index.ts rename to packages/transpiler/babel-inula-next-core/src/analyze/index.ts index 63f4ac70..4f602a4f 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/index.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/index.ts @@ -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; } diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/nodeFactory.ts b/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts similarity index 76% rename from packages/transpiler/babel-inula-next-core/src/analyzer/nodeFactory.ts rename to packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts index bacd3695..3d797a84 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/nodeFactory.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts @@ -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(), _reactiveBitMap: parent ? new Map(parent._reactiveBitMap) : new Map(), 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 | NodePath, - deps: Set, - 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, 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; } diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/pruneComponentUnusedBit.ts b/packages/transpiler/babel-inula-next-core/src/analyze/pruneUnusedBit.ts similarity index 55% rename from packages/transpiler/babel-inula-next-core/src/analyzer/pruneComponentUnusedBit.ts rename to packages/transpiler/babel-inula-next-core/src/analyze/pruneUnusedBit.ts index 2bd1737b..7a0b236a 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/pruneComponentUnusedBit.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/pruneUnusedBit.ts @@ -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(); - 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; -} diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/types.ts b/packages/transpiler/babel-inula-next-core/src/analyze/types.ts similarity index 81% rename from packages/transpiler/babel-inula-next-core/src/analyzer/types.ts rename to packages/transpiler/babel-inula-next-core/src/analyze/types.ts index b87ad2af..3e1a489f 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/types.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/types.ts @@ -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 { @@ -32,16 +43,14 @@ export interface ReactiveVariable extends BaseVariable { 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 { @@ -67,10 +76,6 @@ export interface ComponentNode { level: number; // The variables defined in the component variables: Variable[]; - /** - * The used properties in the component - */ - usedPropertySet: Set; usedBit: Bitmap; /** * The map to find the reactive bitmap by name @@ -95,7 +100,7 @@ export interface ComponentNode { * The watch fn in the component */ watch?: { - depMask?: Bitmap; + dependency: Dependency | null; callback: NodePath | NodePath; }[]; } diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/utils.ts b/packages/transpiler/babel-inula-next-core/src/analyze/utils.ts similarity index 100% rename from packages/transpiler/babel-inula-next-core/src/analyzer/utils.ts rename to packages/transpiler/babel-inula-next-core/src/analyze/utils.ts diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/reactive/getDependencies.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/reactive/getDependencies.ts deleted file mode 100644 index 19dbcf16..00000000 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/reactive/getDependencies.ts +++ /dev/null @@ -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, - { current }: AnalyzeContext -) { - // ---- Deps: console.log(count) - let depMask = 0; - // ---- Assign deps: count = 1 or count++ - let assignDepMask = 0; - const depNodes: Record = {}; - const deps = new Set(); - - const visitor = (innerPath: NodePath) => { - 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) - ); -} diff --git a/packages/transpiler/babel-inula-next-core/src/generate/index.ts b/packages/transpiler/babel-inula-next-core/src/generate/index.ts new file mode 100644 index 00000000..840ee4dc --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/src/generate/index.ts @@ -0,0 +1,9 @@ +import { ViewParticle } from '@openinula/reactivity-parser'; +import { ComponentNode } from '../analyze/types'; + +type Visitor = { + [Type in (ViewParticle | ComponentNode)['type']]: ( + node: Extract, + ctx: any + ) => void; +}; diff --git a/packages/transpiler/babel-inula-next-core/src/generator/index.ts b/packages/transpiler/babel-inula-next-core/src/generator/index.ts deleted file mode 100644 index c7551d26..00000000 --- a/packages/transpiler/babel-inula-next-core/src/generator/index.ts +++ /dev/null @@ -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, - 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>) { - const reversedMap: Record> = {}; - 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 | 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); -} diff --git a/packages/transpiler/babel-inula-next-core/src/index.ts b/packages/transpiler/babel-inula-next-core/src/index.ts index 07bdc408..f3fa9947 100644 --- a/packages/transpiler/babel-inula-next-core/src/index.ts +++ b/packages/transpiler/babel-inula-next-core/src/index.ts @@ -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; -} diff --git a/packages/transpiler/babel-inula-next-core/src/plugin.ts b/packages/transpiler/babel-inula-next-core/src/plugin.ts index 830787f1..a677a894 100644 --- a/packages/transpiler/babel-inula-next-core/src/plugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/plugin.ts @@ -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'; diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/lifeCycle.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/lifeCycle.test.ts index b7b33319..949bef52 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/lifeCycle.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/lifeCycle.test.ts @@ -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]); diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts index fa4df586..0e7e40db 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts @@ -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); }); }); diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/pruneBitmap.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/pruneBitmap.test.ts index dd17edbe..606fe7cb 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/pruneBitmap.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/pruneBitmap.test.ts @@ -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 {count}{doubleCount}; + let count = 1; // 0b1000 + const db = count * 2; // 0b10000 + return {count}{db * count}; }); return
{doubleCount}
; }); `); - 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); }); }); diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/utils.ts b/packages/transpiler/babel-inula-next-core/test/analyze/utils.ts new file mode 100644 index 00000000..996f3265 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/analyze/utils.ts @@ -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; +} diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts index d0e4e075..2ed52525 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts @@ -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]"'); }); diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts index a39ecae1..b8000108 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts @@ -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); }); }); diff --git a/packages/transpiler/babel-inula-next-core/test/mock.ts b/packages/transpiler/babel-inula-next-core/test/mock.ts index 596ebccf..60947be8 100644 --- a/packages/transpiler/babel-inula-next-core/test/mock.ts +++ b/packages/transpiler/babel-inula-next-core/test/mock.ts @@ -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'; diff --git a/packages/transpiler/babel-inula-next-core/tsconfig.json b/packages/transpiler/babel-inula-next-core/tsconfig.json index e0932d78..0cdc98df 100644 --- a/packages/transpiler/babel-inula-next-core/tsconfig.json +++ b/packages/transpiler/babel-inula-next-core/tsconfig.json @@ -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 - } -} \ No newline at end of file + }, + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/packages/transpiler/babel-preset-inula-next/CHANGELOG.md b/packages/transpiler/babel-preset-inula-next/CHANGELOG.md deleted file mode 100644 index 263ea4f5..00000000 --- a/packages/transpiler/babel-preset-inula-next/CHANGELOG.md +++ /dev/null @@ -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 diff --git a/packages/transpiler/babel-preset-inula-next/package.json b/packages/transpiler/babel-preset-inula-next/package.json deleted file mode 100644 index 1bd04635..00000000 --- a/packages/transpiler/babel-preset-inula-next/package.json +++ /dev/null @@ -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 - } -} diff --git a/packages/transpiler/babel-preset-inula-next/src/analyze/earlyReturnAnalyze.ts b/packages/transpiler/babel-preset-inula-next/src/analyze/earlyReturnAnalyze.ts deleted file mode 100644 index 842993d1..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/analyze/earlyReturnAnalyze.ts +++ /dev/null @@ -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, context: AnalyzeContext) { - const currentComp = context.currentComponent; - - const argument = path.get('argument'); - if (argument.isJSXElement()) { - currentComp.child = createJSXNode(currentComp, argument); - } - }, - IfStatement(ifStmt: NodePath, context: AnalyzeContext) { - if (!hasEarlyReturn(ifStmt)) { - return; - } - const currentComp = context.currentComponent; - - const branches: Branch[] = []; - let next: NodePath | 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 = 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) { - return next.isBlockStatement() ? next.get('body') : [next]; -} - -function hasEarlyReturn(path: NodePath) { - let hasReturn = false; - path.traverse({ - ReturnStatement(path: NodePath) { - if ( - path.parentPath.isFunctionDeclaration() || - path.parentPath.isFunctionExpression() || - path.parentPath.isArrowFunctionExpression() - ) { - return; - } - hasReturn = true; - }, - }); - return hasReturn; -} diff --git a/packages/transpiler/babel-preset-inula-next/src/analyze/index.ts b/packages/transpiler/babel-preset-inula-next/src/analyze/index.ts deleted file mode 100644 index 28d9613a..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/analyze/index.ts +++ /dev/null @@ -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[], componentNode: ComponentNode, level = 0) { - const visitor = mergeVisitor(...analyzers); - const visit = (p: NodePath, 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, 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, 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; -} diff --git a/packages/transpiler/babel-preset-inula-next/src/analyze/jsxSliceAnalyze.ts b/packages/transpiler/babel-preset-inula-next/src/analyze/jsxSliceAnalyze.ts deleted file mode 100644 index 0afdda3e..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/analyze/jsxSliceAnalyze.ts +++ /dev/null @@ -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, 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) { - // 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 =
` - * 2. SubComponent, like `function Sub() { return
}` - * - * i.e. - * ```jsx - * let jsxSlice =
{count}
- * // => - * function Comp_$id$() { - * return
{count}
- * } - * let jsxSlice = - * ``` - */ -export function jsxSlicesAnalyze(): Visitor { - return { - JSXElement(path: NodePath, ctx) { - const name = genNameFromJSX(path, ctx); - replaceJSXSliceWithSubComp(name, ctx, path); - path.skip(); - }, - JSXFragment(path: NodePath, ctx) { - replaceJSXSliceWithSubComp('frag', ctx, path); - }, - }; -} diff --git a/packages/transpiler/babel-preset-inula-next/src/analyze/lifeCycleAnalyze.ts b/packages/transpiler/babel-preset-inula-next/src/analyze/lifeCycleAnalyze.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/transpiler/babel-preset-inula-next/src/analyze/nodeFactory.ts b/packages/transpiler/babel-preset-inula-next/src/analyze/nodeFactory.ts deleted file mode 100644 index 354314fb..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/analyze/nodeFactory.ts +++ /dev/null @@ -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[], - 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) { - comp.methods.push(method); -} - -export function createJSXNode(parent: ComponentNode, content: NodePath): 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, - }; -} diff --git a/packages/transpiler/babel-preset-inula-next/src/analyze/propertiesAnalyze.ts b/packages/transpiler/babel-preset-inula-next/src/analyze/propertiesAnalyze.ts deleted file mode 100644 index d585983c..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/analyze/propertiesAnalyze.ts +++ /dev/null @@ -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 =
` -// 2. SubComponent, like `function Sub() { return
}` -function handleFn(fnName: string, fnBody: NodePath) { - 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
{count}
- // } - // => - // function jsxFunc() { - // function Comp_$id4$() { - // return
{count}
- // } - // // This is a function that returns JSX - // // because the function name is smallCamelCased - // return - // } - } -} - -// 3. jsx creation function, like `function create() { return
}` -export function propertiesAnalyze(): Visitor { - return { - VariableDeclaration(path: NodePath, 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 = () =>
` - 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); - } - // handle jsx slice - ctx.traverse(path, ctx); - } - addState(ctx.currentComponent, id.node.name, declaration.node.init || null); - } - }); - }, - FunctionDeclaration(path: NodePath, 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')); - }, - }; -} diff --git a/packages/transpiler/babel-preset-inula-next/src/analyze/propsAnalyze.ts b/packages/transpiler/babel-preset-inula-next/src/analyze/propsAnalyze.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/transpiler/babel-preset-inula-next/src/analyze/types.ts b/packages/transpiler/babel-preset-inula-next/src/analyze/types.ts deleted file mode 100644 index 67453dfe..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/analyze/types.ts +++ /dev/null @@ -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; - // A valuable could be a state or computed - valuable: Reactive; - methods: NodePath[]; - child?: InulaNode; - subComponents: ComponentNode[]; - parent?: ComponentNode; - /** - * The function body of the fn component code - */ - // fnBody: NodePath[]; - // a map to find the state - reactiveMap: Record; - level: number; -} - -export interface SubCompNode { - type: 'subComp'; - name: string; - parent: ComponentNode; - child: JSX; -} - -export interface JSXNode { - type: 'jsx'; - parent: ComponentNode; - child: NodePath; -} - -export interface CondNode { - type: 'cond'; - branches: Branch[]; - parent: ComponentNode; - /** - * The default branch - */ - child: InulaNode; -} - -export interface Branch { - conditions: NodePath[]; - content: InulaNode; -} - -export interface AnalyzeContext { - level: number; - index: number; - currentComponent: ComponentNode; - restStmt: NodePath[]; - // --- flow control --- - /** - * ignore the rest of the statements - */ - skipRest: () => void; - traverse: (p: NodePath, ctx: AnalyzeContext) => void; -} - -export type Visitor = { - [Type in Node['type']]?: (path: NodePath>, state: S) => void; -}; -export type Analyzer = () => Visitor; - -export interface FnComponentDeclaration extends t.FunctionDeclaration { - id: t.Identifier; -} diff --git a/packages/transpiler/babel-preset-inula-next/src/analyze/utils.ts b/packages/transpiler/babel-preset-inula-next/src/analyze/utils.ts deleted file mode 100644 index 5795e3ca..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/analyze/utils.ts +++ /dev/null @@ -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(path: NodePath): path is NodePath> { - 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) { - if (path.isJSXElement()) { - return true; - } - - // check if there is JSXElement in the children - let seen = false; - path.traverse({ - JSXElement() { - seen = true; - }, - }); - return seen; -} diff --git a/packages/transpiler/babel-preset-inula-next/src/const.ts b/packages/transpiler/babel-preset-inula-next/src/const.ts deleted file mode 100644 index 461f7594..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/const.ts +++ /dev/null @@ -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: ['*'], -}; diff --git a/packages/transpiler/babel-preset-inula-next/src/global.d.ts b/packages/transpiler/babel-preset-inula-next/src/global.d.ts deleted file mode 100644 index e01cb60d..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/global.d.ts +++ /dev/null @@ -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'; \ No newline at end of file diff --git a/packages/transpiler/babel-preset-inula-next/src/index.ts b/packages/transpiler/babel-preset-inula-next/src/index.ts deleted file mode 100644 index c50d8feb..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/index.ts +++ /dev/null @@ -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; -} diff --git a/packages/transpiler/babel-preset-inula-next/src/main.ts b/packages/transpiler/babel-preset-inula-next/src/main.ts deleted file mode 100644 index aba8dd59..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/main.ts +++ /dev/null @@ -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), - }, - }; -} diff --git a/packages/transpiler/babel-preset-inula-next/src/plugin.ts b/packages/transpiler/babel-preset-inula-next/src/plugin.ts deleted file mode 100644 index e5778cd5..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/plugin.ts +++ /dev/null @@ -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), - }, - }; -} diff --git a/packages/transpiler/babel-preset-inula-next/src/pluginProvider.ts b/packages/transpiler/babel-preset-inula-next/src/pluginProvider.ts deleted file mode 100644 index 1a5af5d9..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/pluginProvider.ts +++ /dev/null @@ -1,1515 +0,0 @@ -import type babel from '@babel/core'; -import { type types as t, type NodePath } from '@babel/core'; -import { type PropertyContainer, type HTMLTags, type SnippetPropSubDepMap } from './types'; -import { minimatch } from 'minimatch'; -import { parseView, ViewUnit } from '@openinula/view-parser'; -import { parseView as parseJSX } from '@openinula/jsx-view-parser'; -import { parseReactivity } from '@openinula/reactivity-parser'; -import { generateSnippet, generateView } from '@openinula/view-generator'; -import { - alterAttributeMap, - availableDecoNames, - defaultHTMLTags, - devMode, - dlightDefaultPackageName, - importMap, - importsToDelete, - reactivityFuncNames, -} from './const'; - -export class PluginProvider { - private readonly dlightPackageName = dlightDefaultPackageName; - - // ---- Plugin Level - private readonly babelApi: typeof babel; - private readonly t: typeof t; - private readonly traverse: typeof babel.traverse; - private readonly enableDevTools: boolean; - private readonly includes: string[]; - private readonly excludes: string[]; - private readonly htmlTags: string[]; - private readonly attributeMap: Record; - // when parsing JSX, use JSX parser instead of the default parser - private isJsx = false; - - constructor( - babelApi: typeof babel, - types: typeof t, - includes: string[], - excludes: string[], - enableDevTools: boolean, - htmlTags: HTMLTags, - attributeMap: Record - ) { - this.babelApi = babelApi; - this.t = types; - this.traverse = babelApi.traverse; - this.includes = includes; - this.excludes = excludes; - this.enableDevTools = devMode && enableDevTools; - this.htmlTags = - typeof htmlTags === 'function' - ? htmlTags(defaultHTMLTags) - : htmlTags.includes('*') - ? [...new Set([...defaultHTMLTags, ...htmlTags])].filter(tag => tag !== '*') - : htmlTags; - this.attributeMap = attributeMap; - } - - // ---- DLight class Level - private classDeclarationNode?: t.ClassDeclaration; - private classBodyNode?: t.ClassBody; - private propertiesContainer: PropertyContainer = {}; - private dependencyMap: Record = {}; - private enter = true; - private dLightModel = false; - private enterClassNode = false; - private className?: string; - - // ---- File Level - private programNode?: t.Program; - private allImports: t.ImportDeclaration[] = []; - private didAlterImports = false; - - /* ---- DLight Class Level Hooks ---- */ - /** - * @brief Clear all DLight Node Level variables after a class is transformed - */ - clearNode() { - this.classDeclarationNode = undefined; - this.classBodyNode = undefined; - this.propertiesContainer = {}; - this.dependencyMap = {}; - this.enter = true; - this.enterClassNode = false; - this.dLightModel = false; - this.className = undefined; - } - - private get availableProperties(): string[] { - return Object.entries(this.propertiesContainer) - .filter( - ([key, { isWatcher, isStatic, isChildren }]) => key !== '_$compName' && !isWatcher && !isStatic && !isChildren - ) - .map(([key]) => key); - } - - /** - * @brief Initialize DLight Node Level variables when entering a class - * @param path - */ - initNode(path: NodePath): void { - const node: t.ClassDeclaration = path.node; - this.classDeclarationNode = node; - this.classBodyNode = node.body; - this.propertiesContainer = {}; - - if (!node.id?.name) { - node.id = this.t.identifier(`Anonymous_${PluginProvider.uid()}`); - } - this.className = node.id?.name; - - // ---- Custom decorators - this.handleClassCustomDecorators(); - - // ---- If devtools is enabled, add _$compName property to the class - if (this.enableDevTools) { - this.classBodyNode.body.unshift( - this.t.classProperty(this.t.identifier('_$compName'), this.t.stringLiteral(this.className)) - ); - } - - // ---- Add dlight import and alter import name, - // Only do this when enter the first dlight class - if (!this.didAlterImports) { - // ---- Get DLight imports - const dlightImports = this.allImports.filter(n => n.source.value === dlightDefaultPackageName); - // ---- Alter import name, e.g. "@dlight/dlight-client" - if (this.dlightPackageName !== dlightDefaultPackageName) { - dlightImports.forEach(i => { - i.source.value = this.dlightPackageName; - }); - } - - // ---- remove all tag-import - dlightImports.forEach(importNode => { - importNode.specifiers = importNode.specifiers.filter( - specifier => - !( - this.t.isImportSpecifier(specifier) && - this.t.isIdentifier(specifier.imported) && - importsToDelete.includes(specifier.imported.name) - ) - ); - }); - - // ---- Add nodes import to the head of file - this.programNode!.body.unshift( - this.t.importDeclaration( - Object.entries(importMap).map(([key, value]) => - this.t.importSpecifier(this.t.identifier(value), this.t.identifier(key)) - ), - this.t.stringLiteral(this.dlightPackageName) - ) - ); - this.didAlterImports = true; - } - } - - /* ---- Babel Visitors ---- */ - programEnterVisitor(path: NodePath, filename: string | undefined): void { - this.enter = this.fileAllowed(filename); - if (!this.enter) return; - // switch to JSX parser according to the file extension - if ((filename && filename.endsWith('.jsx')) || (filename && filename.endsWith('.tsx'))) { - this.isJsx = true; - } - - this.allImports = path.node.body.filter(n => this.t.isImportDeclaration(n)) as t.ImportDeclaration[]; - // const dlightImports = this.allImports.filter( - // n => n.source.value === dlightDefaultPackageName - // ) - // if (dlightImports.length === 0) { - // this.enter = false - // return - // } - this.programNode = path.node; - } - - programExitVisitor(): void { - if (!this.enter) return; - this.didAlterImports = false; - this.allImports = []; - this.programNode = undefined; - } - - classEnter(path: NodePath): void { - if (!this.enter) return; - this.enterClassNode = this.isDLightClass(path); - if (!this.enterClassNode) return; - this.initNode(path); - this.resolveMounting(path); - } - - classExit(): void { - if (!this.enter) return; - if (!this.enterClassNode) return; - this.transformDLightClass(); - this.clearNode(); - this.enterClassNode = false; - } - - classMethodVisitor(path: NodePath): void { - if (!this.enterClassNode) return; - if (!this.t.isIdentifier(path.node.key)) return; - const key = path.node.key.name; - if (key === 'Body') return; - - const isSnippet = this.findDecoratorByName(path.node.decorators, 'Snippet'); - if (isSnippet) return; - const node = path.node; - - // ---- Handle watcher - // ---- Get watcher decorator or watcher function decorator - // ---- Watcher auto collect deps: - // @Watch - // watcher() { myFunc() } - // ---- Watcher function manual set deps: - // @Watch(["count", "flag"]) - // watcherFunc() { myFunc() } - const watchDeco = this.findDecoratorByName(node.decorators, 'Watch'); - if (this.t.isIdentifier(node.key, { name: 'constructor' })) return; - this.autoBindMethods(node); - if (!watchDeco) return; - - // ---- Get dependencies from watcher decorator or watcher function decorator - let deps: string[] = []; - let depsNode; - if (this.t.isIdentifier(watchDeco)) { - [deps, depsNode] = this.getDependencies(node); - } else { - const listenDepStrings = watchDeco.arguments - .filter(arg => this.t.isStringLiteral(arg)) - .map(arg => (arg as t.StringLiteral).value); - const pseudoMethod = this.t.classMethod( - 'method', - node.key, - [], - this.t.blockStatement([ - this.t.expressionStatement( - this.t.arrayExpression( - listenDepStrings.map(str => this.t.memberExpression(this.t.thisExpression(), this.t.identifier(str))) - ) - ), - ]) - ); - - [deps, depsNode] = this.getDependencies(pseudoMethod); - } - // ---- Register watcher to propertiesContainer - this.propertiesContainer[key] = { - node, - deps, - depsNode, - isWatcher: true, - }; - node.decorators = this.removeDecorators(node.decorators, ['Watch']); - } - - classPropertyVisitor(path: NodePath): void { - if (!this.enterClassNode) return; - const node = path.node; - if (!this.t.isIdentifier(node.key)) return; - const key = node.key.name; - if (key === 'Body') return; - const decorators = node.decorators; - const isSnippet = this.findDecoratorByName(decorators, 'Snippet'); - if (isSnippet) return; - // ---- Parse model - const isModel = this.parseModel(path); - - const isProp = !!this.findDecoratorByName(decorators, 'Prop'); - const isEnv = !!this.findDecoratorByName(decorators, 'Env'); - - const isChildren = !!this.findDecoratorByName(node.decorators, 'Children'); - - const [deps, depsNode] = !isChildren ? this.getDependencies(node) : [[]]; - - this.propertiesContainer[key] = { - node, - deps, - depsNode, - isStatic: !!this.findDecoratorByName(decorators, 'Static'), - isContent: !!this.findDecoratorByName(decorators, 'Content'), - isChildren, - isPropOrEnv: isProp ? 'Prop' : isEnv ? 'Env' : undefined, - isModel, - }; - - node.decorators = this.removeDecorators(decorators, availableDecoNames); - } - - /* ---- Decorator Resolvers ---- */ - /** - * @brief Decorator resolver: Watcher - * Add: - * $wW${key} - * in watcher: - * watchxx() { - * if (this._$cache(${key}, ${deps})) return - * ... - * } - * @param node - */ - resolveWatcherDecorator(node: t.ClassMethod, depsNode: t.ArrayExpression): void { - if (!this.t.isIdentifier(node.key)) return; - const key = node.key.name; - const propertyIdx = this.classBodyNode!.body.indexOf(node); - const watcherNode = this.t.classProperty(this.t.identifier(`$w$${key}`)); - this.classBodyNode!.body.splice(propertyIdx, 0, watcherNode); - node.body.body.unshift( - this.t.ifStatement( - this.t.callExpression(this.t.memberExpression(this.t.thisExpression(), this.t.identifier('_$cache')), [ - this.t.stringLiteral(key), - depsNode, - ]), - this.t.blockStatement([this.t.returnStatement()]) - ) - ); - } - - /** - * @brief Decorator resolver: Children - * Add: - * get ${key}() { - * return this._$children - * } - * @param node - */ - resolveChildrenDecorator(node: t.ClassProperty) { - if (!this.classBodyNode) return; - if (!this.t.isIdentifier(node.key)) return; - const key = node.key.name; - const propertyIdx = this.classBodyNode.body.indexOf(node); - - const childrenFuncCallNode = this.t.memberExpression(this.t.thisExpression(), this.t.identifier('_$children')); - - const getterNode = this.t.classMethod( - 'get', - this.t.identifier(key), - [], - this.t.blockStatement([this.t.returnStatement(childrenFuncCallNode)]) - ); - this.classBodyNode.body.splice(propertyIdx, 1, getterNode); - } - - /** - * @brief Decorator resolver: Content - * Add: - * _$contentKey = "key" - * @param node - */ - resolveContentDecorator(node: t.ClassProperty) { - if (!this.classBodyNode) return; - if (!this.t.isIdentifier(node.key)) return; - - // ---- Already has _$contentKey - if (this.classBodyNode.body.some(n => this.t.isClassProperty(n) && (n.key as t.Identifier).name === '_$contentKey')) - return; - const key = node.key.name; - const propertyIdx = this.classBodyNode.body.indexOf(node); - - const derivedStatusKey = this.t.classProperty(this.t.identifier('_$contentKey'), this.t.stringLiteral(key)); - this.classBodyNode.body.splice(propertyIdx, 0, derivedStatusKey); - } - - /** - * @brief Decorator resolver: Prop/Env - * Add: - * $p/e$${key} - * @param node - */ - resolvePropDecorator(node: t.ClassProperty, decoratorName: 'Prop' | 'Env') { - if (!this.classBodyNode) return; - if (!this.t.isIdentifier(node.key)) return; - const key = node.key.name; - const propertyIdx = this.classBodyNode.body.indexOf(node); - const tag = decoratorName.toLowerCase() === 'prop' ? 'p' : 'e'; - const derivedStatusKey = this.t.classProperty(this.t.identifier(`$${tag}$${key}`)); - this.classBodyNode.body.splice(propertyIdx, 0, derivedStatusKey); - } - - /** - * @brief Decorator resolver: State - * Add: - * $$${key} = ${depIdx} - * $sub$${key} = [${reversedDeps}] - * @param node - */ - resolveStateDecorator(node: t.ClassProperty, idx: number, reverseDeps: Set | undefined) { - if (!this.classBodyNode) return; - if (!this.t.isIdentifier(node.key)) return; - const key = node.key.name; - const propertyIdx = this.classBodyNode.body.indexOf(node); - - const idxNode = !this.dLightModel - ? [this.t.classProperty(this.t.identifier(`$$${key}`), this.t.numericLiteral(1 << idx))] - : []; - - const depsNode = reverseDeps - ? [ - this.t.classProperty( - this.t.identifier(`$s$${key}`), - this.t.arrayExpression([...reverseDeps].map(d => this.t.stringLiteral(d))) - ), - ] - : []; - - this.classBodyNode.body.splice(propertyIdx + 1, 0, ...idxNode, ...depsNode); - } - - /* ---- Helper Functions ---- */ - handleClassCustomDecorators() { - if (!this.classBodyNode) return; - const decorators = this.classDeclarationNode?.decorators; - if (!decorators) return; - // ---- Forward Prop - const forwardPropDeco = this.findDecoratorByName(decorators, 'ForwardProps'); - /** - * _$forwardProp - * _$forwardPropMap = new Set() - * _$forwardPropsId = [] - */ - if (forwardPropDeco) { - this.classBodyNode.body.unshift( - this.t.classProperty(this.t.identifier('_$forwardProps')), - this.t.classProperty( - this.t.identifier('_$forwardPropsSet'), - this.t.newExpression(this.t.identifier('Set'), []) - ), - this.t.classProperty(this.t.identifier('_$forwardPropsId'), this.t.arrayExpression([])) - ); - this.classDeclarationNode!.decorators = this.removeDecorators(decorators, ['ForwardProps']); - } - } - - /** - * @brief Transform the whole DLight class when exiting the class - * 1. Alter all the state properties - * 2. Transform MainView and Snippets with DLight syntax - */ - transformDLightClass(): void { - const usedProperties = this.handleView(); - this.addAutoUpdate(this.dLightModel ? this.availableProperties : usedProperties); - const propertyArr = Object.entries(this.propertiesContainer).reverse(); - const depReversedMap = this.dependencyMapReversed(); - - for (const [ - key, - { node, deps, isStatic, isChildren, isPropOrEnv, isWatcher, isContent, isModel, depsNode }, - ] of propertyArr) { - if (isChildren) { - this.resolveChildrenDecorator(node as t.ClassProperty); - continue; - } - if (deps.length > 0) { - usedProperties.push(...deps); - if (isWatcher) { - this.resolveWatcherDecorator(node as t.ClassMethod, depsNode!); - } else if (!isModel) { - this.handleDerivedProperty(node as t.ClassProperty, depsNode!); - } - } - if (isPropOrEnv) { - this.resolvePropDecorator(node as t.ClassProperty, isPropOrEnv); - } - if (isContent) { - this.resolvePropDecorator(node as t.ClassProperty, 'Prop'); - this.resolveContentDecorator(node as t.ClassProperty); - } - if (isStatic) continue; - if (usedProperties.includes(key)) { - this.resolveStateDecorator(node as t.ClassProperty, this.availableProperties.indexOf(key), depReversedMap[key]); - } - } - } - - /** - * @brief Add updateProp and updateDerived if there's a assignment - * @param usedProperties - * @returns - */ - private addAutoUpdate(usedProperties: string[]) { - if (!this.classBodyNode) return; - const nonViewNodes = this.classBodyNode.body.filter( - n => - !( - ((this.t.isClassProperty(n) || this.t.isClassMethod(n)) && - ['constructor', '_$compName'].includes((n.key as t.Identifier).name)) || - this.t.isClassMethod(n, { static: true }) || - this.t.isClassProperty(n, { static: true }) - ) - ); - nonViewNodes.forEach(n => { - const value = this.t.isClassProperty(n) ? n.value : this.t.isClassMethod(n) ? n.body : null; - if (!value) return; - this.addUpdateDerived(value, usedProperties); - }); - } - - /** - * @Brief Add updateView and updateDerived to the node - * @param node - * @param usedProperties - */ - private addUpdateDerived(node: t.Expression | t.BlockStatement, usedProperties: string[]) { - const newUpdateProp = (node: t.Expression, key: string) => - this.t.callExpression(this.t.memberExpression(this.t.thisExpression(), this.t.identifier('_$ud')), [ - node, - this.t.stringLiteral(key), - ]); - this.traverse(this.valueWrapper(node), { - MemberExpression: path => { - if (!this.t.isThisExpression(path.node.object) || !this.t.isIdentifier(path.node.property)) return; - const key = path.node.property.name; - if (!usedProperties.includes(key)) return; - const assignPath = this.isAssignmentExpressionLeft(path); - if (!assignPath) return; - assignPath.replaceWith(newUpdateProp(assignPath.node as t.Expression, key)); - assignPath.skip(); - }, - CallExpression: path => { - if (!this.t.isMemberExpression(path.node.callee)) return; - const funcNameNode = path.node.callee.property; - if (!this.t.isIdentifier(funcNameNode)) return; - if (!reactivityFuncNames.includes(funcNameNode.name)) return; - let callee = path.get('callee').get('object') as NodePath; - - while (this.t.isMemberExpression(callee.node)) { - callee = callee.get('object') as NodePath; - } - if (!this.t.isThisExpression(callee?.node)) return; - const key = ((callee.parentPath!.node as t.MemberExpression).property as t.Identifier).name; - path.replaceWith(newUpdateProp(path.node, key)); - path.skip(); - }, - }); - } - - /* ---- DLight Class View Handlers ---- */ - /** - * @brief Transform Body and Snippets with DLight syntax - * @returns used properties - */ - handleView(): string[] { - if (!this.classBodyNode) return []; - const usedPropertySet = new Set(); - let mainView: undefined | t.ClassMethod; - const snippetNodes: t.ClassMethod[] = []; - for (let viewNode of this.classBodyNode.body) { - if (!this.t.isClassProperty(viewNode) && !this.t.isClassMethod(viewNode)) continue; - if (!this.t.isIdentifier(viewNode.key)) continue; - const isSnippet = this.findDecoratorByName(viewNode.decorators, 'Snippet'); - const isMainView = viewNode.key.name === 'Body'; - if (!isSnippet && !isMainView) continue; - - if (this.t.isClassProperty(viewNode)) { - // ---- Handle TSAsExpression, e.g. MyView = (() => {}) as Type1 as Type2 - let exp = viewNode.value; - while (this.t.isTSAsExpression(exp)) exp = exp.expression; - if (!this.t.isArrowFunctionExpression(exp)) continue; - viewNode.value = exp; - // ---- Transform arrow function property into method - const newViewNode = this.arrowFunctionPropertyToMethod(viewNode); - if (!newViewNode) continue; - viewNode = newViewNode; - } - - if (isSnippet) { - viewNode.decorators = null; - snippetNodes.push(viewNode); - } else { - mainView = viewNode; - } - } - - const snippetNames = snippetNodes.map(v => (v.key as t.Identifier).name); - const snippetPropSubDepMap: SnippetPropSubDepMap = Object.fromEntries( - snippetNodes - .map(v => { - const prop = v.params[0]; - if (!prop || !this.t.isObjectPattern(prop)) return ['-', null as any]; - const props = Object.fromEntries( - prop.properties - .map(p => { - if (!this.t.isObjectProperty(p)) return ['-', null]; - const key = (p.key as t.Identifier).name; - // ---- Get identifiers that depend on this prop - const subDeps = this.getIdentifiers( - // ---- Some unimportant value wrapper - this.t.assignmentExpression( - '=', - this.t.objectPattern([this.t.objectProperty(this.t.numericLiteral(0), p.value)]), - this.t.numericLiteral(0) - ) - ).filter(v => v !== key); - return [key, subDeps]; - }) - .filter(([_, props]) => props) - ); - return [(v.key as t.Identifier).name, props]; - }) - .filter(([_, props]) => props) - ); - let templateIdx = -1; - if (mainView) { - let usedProperties; - [usedProperties, templateIdx] = this.alterMainView(mainView, snippetNames, snippetPropSubDepMap); - usedProperties.forEach(usedPropertySet.add.bind(usedPropertySet)); - } - - snippetNodes.forEach(viewNode => { - let usedProperties; - [usedProperties, templateIdx] = this.alterSnippet(viewNode, snippetNames, snippetPropSubDepMap, templateIdx); - usedProperties.forEach(usedPropertySet.add.bind(usedPropertySet)); - }); - - const usedProperties: string[] = []; - this.availableProperties.forEach(p => { - if (usedPropertySet.has(p)) usedProperties.push(p); - }); - // const usedProperties = usedPropertyDeps.map(dep => dep.slice(1, -4)) - return usedProperties; - } - - /** - * @brief Transform Views with DLight syntax - * @param viewNode - * @param snippetNames - * @param isSnippet - * @returns Used properties - */ - alterMainView( - viewNode: t.ClassMethod, - snippetNames: string[], - snippetPropSubDepMap: SnippetPropSubDepMap - ): [Set, number] { - let viewUnits: ViewUnit[] = []; - // if the body method return a JSX element, parse it - if (this.isJsx) { - // Assume there should not early return - const returnNode = viewNode.body.body.find(v => this.t.isReturnStatement(v)) as t.ReturnStatement; - - // check if the return statement is a JSX element - if (this.t.isJSXElement(returnNode.argument) || this.t.isJSXFragment(returnNode.argument)) { - viewUnits = parseJSX(returnNode.argument, { - babelApi: this.babelApi, - htmlTags: this.htmlTags, - parseTemplate: false, - }); - } - } else { - viewUnits = parseView(viewNode.body, { - babelApi: this.babelApi, - snippetNames: snippetNames, - htmlTags: this.htmlTags, - }); - } - - const [viewParticles, usedPropertySet] = parseReactivity(viewUnits, { - babelApi: this.babelApi, - availableProperties: this.availableProperties, - dependencyMap: this.dependencyMap, - reactivityFuncNames, - }); - - const [body, classProperties, templateIdx] = generateView(viewParticles, { - babelApi: this.babelApi, - className: this.className!, - importMap, - snippetPropMap: Object.fromEntries( - Object.entries(snippetPropSubDepMap).map(([key, props]) => [key, Object.keys(props)]) - ), - templateIdx: -1, - attributeMap: this.attributeMap, - alterAttributeMap, - }); - viewNode.body = body; - this.classBodyNode?.body.push(...classProperties); - - return [usedPropertySet, templateIdx]; - } - - /** - * @brief Transform Snippets with DLight syntax - * @param viewNode - * @param snippetNames - * @param snippetPropSubDepMap - * @param templateIdx - * @returns - */ - alterSnippet( - viewNode: t.ClassMethod, - snippetNames: string[], - snippetPropSubDepMap: SnippetPropSubDepMap, - templateIdx: number - ): [Set, number] { - // ---- Add prop => Sub() => Sub(_$, $snippetNode) - if (viewNode.params.length === 0) { - viewNode.params.push(this.t.identifier('_$'), this.t.identifier('$snippetNode')); - } else if (viewNode.params.length === 1) { - viewNode.params.push(this.t.identifier('$snippetNode')); - } else { - viewNode.params[1] = this.t.identifier('$snippetNode'); - viewNode.params.length = 2; - } - const viewUnits = parseView(viewNode.body, { - babelApi: this.babelApi, - snippetNames: snippetNames, - htmlTags: this.htmlTags, - }); - - const snippetProp = snippetPropSubDepMap[(viewNode.key as t.Identifier).name] ?? []; - const identifierDepMap: Record = {}; - Object.entries(snippetProp).forEach(([key, subDeps]) => { - subDeps.forEach(dep => { - identifierDepMap[dep] = [key]; - }); - }); - - const [viewParticlesProperty, usedPropertySet] = parseReactivity(viewUnits, { - babelApi: this.babelApi, - availableProperties: this.availableProperties, - availableIdentifiers: Object.keys(snippetProp), - dependencyMap: this.dependencyMap, - dependencyParseType: 'property', - reactivityFuncNames, - }); - - const [viewParticlesIdentifier] = parseReactivity(viewUnits, { - babelApi: this.babelApi, - availableProperties: Object.keys(snippetProp), - dependencyMap: this.dependencyMap, - dependencyParseType: 'identifier', - identifierDepMap, - reactivityFuncNames, - }); - - const snippetPropMap = Object.fromEntries( - Object.entries(snippetPropSubDepMap).map(([key, props]) => [key, Object.keys(props)]) - ); - const [body, classProperties, newTemplateIdx] = generateSnippet( - viewParticlesProperty, - viewParticlesIdentifier, - viewNode.params[0] as t.ObjectPattern, - { - babelApi: this.babelApi, - className: this.className!, - importMap, - snippetPropMap, - templateIdx, - attributeMap: this.attributeMap, - alterAttributeMap, - } - ); - viewNode.body = body; - this.classBodyNode?.body.push(...classProperties); - - return [usedPropertySet, newTemplateIdx]; - } - - /** - * @brief Test if the file is allowed to be transformed - * @param fileName - * @returns is file allowed - */ - private fileAllowed(fileName: string | undefined): boolean { - if (this.includes.includes('*')) return true; - if (!fileName) return false; - if (this.excludes.some(pattern => minimatch(fileName, pattern))) return false; - if (!this.includes.some(pattern => minimatch(fileName, pattern))) return false; - return true; - } - - /** - * @brief Test if the class is a dlight view - * @param path - * @returns - */ - private isDLightView(path: NodePath): boolean { - const node = path.node; - const decorators = node.decorators ?? []; - const isViewDecorator = decorators.find((deco: t.Decorator) => - this.t.isIdentifier(deco.expression, { name: 'View' }) - ); - if (isViewDecorator) { - node.superClass = this.t.identifier('View'); - node.decorators = node.decorators?.filter( - (deco: t.Decorator) => !this.t.isIdentifier(deco.expression, { name: 'View' }) - ); - } - return this.t.isIdentifier(node.superClass, { name: 'View' }); - } - - /** - * @brief Test if the class is a dlight model - * @param path - * @returns - */ - private isDLightModel(path: NodePath): boolean { - const node = path.node; - const decorators = node.decorators ?? []; - const isModelDecorator = decorators.find((deco: t.Decorator) => - this.t.isIdentifier(deco.expression, { name: 'Model' }) - ); - if (isModelDecorator) { - node.superClass = this.t.identifier('Model'); - node.decorators = node.decorators?.filter( - (deco: t.Decorator) => !this.t.isIdentifier(deco.expression, { name: 'Model' }) - ); - } - - // ---- Add property _$model - node.body.body.unshift(this.t.classProperty(this.t.identifier('_$model'))); - - // ---- Delete all views - node.body.body = node.body.body.filter( - n => - !( - (this.t.isClassProperty(n) || this.t.isClassMethod(n, { kind: 'method' })) && - (this.findDecoratorByName(n.decorators, 'Snippet') || (this.t.isIdentifier(n.key) && n.key.name === 'Body')) - ) - ); - this.dLightModel = true; - - return this.t.isIdentifier(node.superClass, { name: 'Model' }); - } - - /** - * @brief Test if the class is a dlight class - * @param path - * @returns - */ - isDLightClass(path: NodePath): boolean { - return this.isDLightView(path) || this.isDLightModel(path); - } - - /** - * @brief Parse any use(Model) inside a property - * @param path - * @returns - */ - private parseModel(path: NodePath) { - const hasUseImport = this.allImports.some( - imp => - imp.source.value === this.dlightPackageName && - imp.specifiers.some(s => { - if (this.t.isImportSpecifier(s) && this.t.isIdentifier(s.imported, { name: 'use' })) { - return true; - } - }) - ); - if (!hasUseImport) return; - const node = path.node; - const key = node.key; - if (!this.t.isIdentifier(key)) return; - const value = node.value; - if (!this.t.isCallExpression(value)) return; - if (!this.t.isIdentifier(value.callee, { name: 'use' })) return; - const args = value.arguments; - const propsArg = args[1]; - const contentArg = args[2]; - let propsNode: t.Expression = this.t.nullLiteral(); - if (propsArg) { - const mergedPropsNode: [t.Expression, t.ArrayExpression | t.NullLiteral][] = []; - const spreadPropsNode: [t.Expression, t.Expression, t.ArrayExpression | t.NullLiteral][] = []; - // ---- Get props deps - if (this.t.isObjectExpression(propsArg)) { - propsArg.properties.forEach(prop => { - if (this.t.isSpreadElement(prop)) { - const [, depsNode] = this.getDependenciesFromNode(prop.argument as t.Expression); - mergedPropsNode.push([prop.argument as t.Expression, depsNode ?? this.t.nullLiteral()]); - } else if (this.t.isObjectProperty(prop)) { - const [, depsNode] = this.getDependenciesFromNode(prop.value as t.Expression); - spreadPropsNode.push([ - !prop.computed && this.t.isIdentifier(prop.key) - ? this.t.stringLiteral(prop.key.name) - : (prop.key as t.Expression), - prop.value as t.Expression, - depsNode ?? this.t.nullLiteral(), - ]); - } else { - spreadPropsNode.push([ - !prop.computed && this.t.isIdentifier(prop.key) - ? this.t.stringLiteral(prop.key.name) - : (prop.key as t.Expression), - this.t.arrowFunctionExpression([], prop.body), - this.t.nullLiteral(), - ]); - } - }); - } else { - const [, depsNode] = this.getDependenciesFromNode(propsArg as t.Expression); - mergedPropsNode.push([propsArg as t.Expression, depsNode ?? this.t.nullLiteral()]); - } - /** - * @View { ok: this.count, ...this.props } - * { - * m: [[this.props, []]] - * s: [["ok", this.count, [this.count]]] - * } - */ - propsNode = this.t.objectExpression([ - this.t.objectProperty( - this.t.identifier('m'), - this.t.arrayExpression(mergedPropsNode.map(n => this.t.arrayExpression(n))) - ), - this.t.objectProperty( - this.t.identifier('s'), - this.t.arrayExpression(spreadPropsNode.map(n => this.t.arrayExpression(n))) - ), - ]); - } - - let contentNode: t.Expression = this.t.nullLiteral(); - if (contentArg) { - const [, depsNode] = this.getDependenciesFromNode(contentArg as t.Expression); - contentNode = this.t.arrayExpression([contentArg as t.Expression, depsNode ?? this.t.nullLiteral()]); - } - args[1] = this.t.arrowFunctionExpression([], propsNode); - args[2] = this.t.arrowFunctionExpression([], contentNode); - args[3] = this.t.stringLiteral(key.name); - value.callee = this.t.memberExpression(this.t.thisExpression(), this.t.identifier('_$injectModel')); - // ---- Wrap a function for lazy evaluation - node.value = this.t.arrowFunctionExpression([], value); - // ---- Add $md$${key} - const propertyIdx = this.classBodyNode!.body.indexOf(node); - const modelDecorator = this.t.classProperty(this.t.identifier(`$md$${key.name}`)); - this.classBodyNode!.body.splice(propertyIdx, 0, modelDecorator); - return true; - } - - /** - * @brief Remove decorators by name - * Only search for Identifier and CallExpression, e.g, @Ok, @Ok() - * @param decorators - * @param names - * @returns new decorators - */ - private removeDecorators(decorators: t.Decorator[] | undefined | null, names: string[]): t.Decorator[] { - if (!decorators) return []; - return decorators.filter( - d => - !( - (this.t.isIdentifier(d.expression) && names.includes(d.expression.name)) || - (this.t.isCallExpression(d.expression) && - this.t.isIdentifier(d.expression.callee) && - names.includes(d.expression.callee.name)) - ) - ); - } - - /** - * @brief Find decorator by name, - * Only search for Identifier and CallExpression, e.g, @Ok, @Ok() - * @param decorators - * @param name - * @returns Identifier or CallExpression or nothing - */ - private findDecoratorByName( - decorators: t.Decorator[] | undefined | null, - name: string - ): t.Identifier | t.CallExpression | undefined { - if (!decorators) return; - return decorators.find( - deco => - this.t.isIdentifier(deco.expression, { name }) || - (this.t.isCallExpression(deco.expression) && this.t.isIdentifier(deco.expression.callee, { name })) - )?.expression as t.Identifier | t.CallExpression | undefined; - } - - /** - * @brief Generate a dependency node from a dependency identifier, - * loop until the parent node is not a binary expression or a member expression - * @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; - } - - /** - * constructor() { - * super() - * } - */ - private addConstructor(): t.ClassMethod { - let constructor = this.classBodyNode!.body.find(n => - this.t.isClassMethod(n, { kind: 'constructor' }) - ) as t.ClassMethod; - if (constructor) return constructor; - - constructor = this.t.classMethod( - 'constructor', - this.t.identifier('constructor'), - [], - this.t.blockStatement([this.t.expressionStatement(this.t.callExpression(this.t.super(), []))]) - ); - - this.classBodyNode!.body.unshift(constructor); - return constructor; - } - - private autoBindMethods(node: t.ClassMethod) { - const constructorNode = this.addConstructor(); - constructorNode.body.body.push( - this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.memberExpression(this.t.thisExpression(), node.key), - this.t.callExpression( - this.t.memberExpression( - this.t.memberExpression(this.t.thisExpression(), node.key), - this.t.identifier('bind') - ), - [this.t.thisExpression()] - ) - ) - ) - ); - } - - /** - * ${key} - * get $f$${key}() { - * if (this._$cache(${key}, ${deps})) return this.${key} - * return ${value} - * } - */ - private handleDerivedProperty(node: t.ClassProperty, depsNode: t.ArrayExpression) { - if (!this.t.isIdentifier(node.key)) return; - const key = node.key.name; - const value = node.value; - const propertyIdx = this.classBodyNode!.body.indexOf(node); - const getterNode = this.t.classMethod( - 'get', - this.t.identifier(`$f$${key}`), - [], - this.t.blockStatement([ - this.t.ifStatement( - this.t.callExpression(this.t.memberExpression(this.t.thisExpression(), this.t.identifier('_$cache')), [ - this.t.stringLiteral(key), - depsNode, - ]), - this.t.blockStatement([ - this.t.returnStatement(this.t.memberExpression(this.t.thisExpression(), this.t.identifier(key))), - ]) - ), - this.t.returnStatement(value), - ]) - ); - this.classBodyNode!.body.splice(propertyIdx + 1, 0, getterNode); - node.value = null; - } - - private getDependenciesFromNode( - node: t.Expression | t.ClassDeclaration, - isClassLevel = false - ): [string[], t.ArrayExpression | undefined] { - // ---- Deps: console.log(this.count) - const deps = new Set(); - // ---- Assign deps: this.count = 1 / this.count++ - const assignDeps = new Set(); - const depNodes: Record = {}; - - this.traverse(this.valueWrapper(node), { - MemberExpression: innerPath => { - if (!this.t.isIdentifier(innerPath.node.property) || !this.t.isThisExpression(innerPath.node.object)) return; - - const propertyKey = innerPath.node.property.name; - if (this.isAssignmentExpressionLeft(innerPath) || this.isAssignmentFunction(innerPath)) { - assignDeps.add(propertyKey); - } else if ( - this.availableProperties.includes(propertyKey) && - !this.isMemberInEscapeFunction(innerPath, this.classDeclarationNode!) && - !this.isMemberInManualFunction(innerPath, this.classDeclarationNode!) - ) { - deps.add(propertyKey); - if (isClassLevel) this.dependencyMap[propertyKey]?.forEach(deps.add.bind(deps)); - if (!depNodes[propertyKey]) depNodes[propertyKey] = []; - depNodes[propertyKey].push(this.geneDependencyNode(innerPath)); - } - }, - }); - - // ---- Eliminate deps that are assigned in the same method - // e.g. { console.log(this.count); this.count = 1 } - // this will cause infinite loop - // so we eliminate "count" from deps - assignDeps.forEach(dep => { - deps.delete(dep); - delete depNodes[dep]; - }); - - 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; - }); - - // ---- Add deps to dependencyMap - const depArr = [...deps]; - if (isClassLevel && deps.size > 0) { - const propertyKey = (((node as t.ClassDeclaration).body.body[0] as t.ClassMethod).key as t.Identifier).name; - this.dependencyMap[propertyKey] = depArr; - } - - return [depArr, this.t.arrayExpression(dependencyNodes)]; - } - /** - * @brief Get all valid dependencies of a babel path - * @param path - * @returns dependencies - */ - private getDependencies(node: t.ClassMethod | t.ClassProperty): [string[], t.ArrayExpression | undefined] { - if (!this.t.isIdentifier(node.key)) return [[], undefined]; - const wrappedNode = this.t.classDeclaration(null, null, this.t.classBody([node])); - return this.getDependenciesFromNode(wrappedNode, true); - } - - private dependencyMapReversed() { - const reversedMap: Record> = {}; - Object.entries(this.dependencyMap).forEach(([key, deps]) => { - deps.forEach(dep => { - if (!reversedMap[dep]) reversedMap[dep] = new Set(); - reversedMap[dep].add(key); - }); - }); - - return reversedMap; - } - - private resolveMounting(path: NodePath) { - const node = path.node; - if (!this.t.isIdentifier(node.id)) return; - const decorators = node.decorators ?? []; - const findEntry = (name: string) => { - const found = decorators.find(deco => this.t.isIdentifier(deco.expression, { name })); - if (found) - decorators.splice( - decorators.findIndex(deco => deco === found), - 1 - ); - return found; - }; - - // ---- Find "@Main" for mounting "main", "@App" for mounting "app" - const entryValue = findEntry('Main') ? 'main' : findEntry('App') ? 'app' : null; - let mountNode: t.Expression; - if (entryValue) { - mountNode = this.t.stringLiteral(entryValue); - } else { - // ---- Find "@Mount("any-id")" - const mounting = decorators.find( - deco => - this.t.isCallExpression(deco.expression) && - this.t.isIdentifier(deco.expression.callee, { name: 'Mount' }) && - deco.expression.arguments.length === 1 - ) as t.Decorator; - if (!mounting) return; - decorators.splice( - decorators.findIndex(deco => deco === mounting), - 1 - ); - mountNode = (mounting.expression as t.CallExpression).arguments[0] as t.Expression; - } - - // ---- ${importMap.render}("main", ${node.id}) - const parentNode = path.parentPath.node; - if (!this.t.isBlockStatement(parentNode) && !this.t.isProgram(parentNode)) return; - const idx = parentNode.body.indexOf(node); - parentNode.body.splice( - idx + 1, - 0, - this.t.expressionStatement( - this.t.callExpression(this.t.identifier(importMap.render), [mountNode, node.id as t.Identifier]) - ) - ); - } - - /** - * @brief Transform arrow function property to method - * @param propertyNode - * @returns new method node - */ - arrowFunctionPropertyToMethod(propertyNode: t.ClassProperty): t.ClassMethod | undefined { - if (!this.t.isArrowFunctionExpression(propertyNode.value)) return; - const value = propertyNode.value; - if (!this.t.isBlockStatement(value.body)) return; - // ---- Remove property - const propertyIdx = this.classBodyNode!.body.indexOf(propertyNode); - // ---- Add method - const methodNode = this.t.classMethod('method', propertyNode.key, value.params, value.body); - this.classBodyNode!.body.splice(propertyIdx, 1, methodNode); - - return methodNode; - } - - /** - * @brief Check if a member expression is a property of a member expression - * @param parentNode - * @param currentNode - * @returns is a property of a member expression - */ - isMemberExpressionProperty(parentNode: t.Node, currentNode: t.Node): boolean { - return this.t.isMemberExpression(parentNode) && !parentNode.computed && parentNode.property === currentNode; - } - - /** - * @brief Check if a member expression is a key of an object - * @param parentNode - * @param currentNode - * @returns is a key of an object - */ - isObjectKey(parentNode: t.Node, currentNode: t.Node): boolean { - return this.t.isObjectProperty(parentNode) && parentNode.key === currentNode; - } - - /** - * @brief Add arrow function to property value - * @param node - */ - valueWithArrowFunc(node: t.ClassProperty): void { - if (!node.value) { - node.value = this.t.identifier('undefined'); - } - node.value = this.t.arrowFunctionExpression([], node.value); - } - - /** - * @brief Get all top level return statements in a block statement, - * ignore nested function returns - * @param node - * @returns - */ - getAllTopLevelReturnBlock(node: t.BlockStatement): t.BlockStatement[] { - const returns: t.BlockStatement[] = []; - - let inNestedFunction = false; - this.traverse(this.valueWrapper(node), { - Function: path => { - if (inNestedFunction) return; - inNestedFunction = true; - path.skip(); - }, - ReturnStatement: path => { - if (inNestedFunction) return; - const parentNode = path.parentPath.node; - if (!this.t.isBlockStatement(parentNode)) { - const newNode = this.t.blockStatement([path.node]); - path.replaceWith(newNode); - returns.push(newNode); - } else { - returns.push(parentNode); - } - path.skip(); - }, - exit: path => { - if (this.t.isFunction(path.node)) inNestedFunction = false; - }, - }); - - return returns; - } - - /** - * @brief Wrap the value in a file - * @param node - * @returns wrapped value - */ - private valueWrapper(node: t.Expression | t.Statement): t.File { - return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)])); - } - - /** - * @brief check if the identifier is from a function param till the stopNode - * e.g: - * function myFunc1(ok) { // stopNode = functionBody - * const myFunc2 = ok => ok // from function param - * console.log(ok) // not from function param - * } - * @param path - * @param idName - */ - private isAttrFromFunction(path: NodePath, idName: string) { - let reversePath = path.parentPath; - - const checkParam: (param: t.Node) => boolean = (param: t.Node) => { - // ---- 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 - .filter(Boolean) - .map(el => checkParam(el!)) - .includes(true); - } - if (this.t.isObjectPattern(param)) { - return ( - param.properties.filter( - prop => this.t.isObjectProperty(prop) && this.t.isIdentifier(prop.key) - ) as t.ObjectProperty[] - ) - .map(prop => (prop.key as t.Identifier).name) - .includes(idName); - } - if (this.t.isRestElement(param)) return checkParam(param.argument); - - return false; - }; - - while (reversePath) { - 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; - } - - return false; - } - - /** - * @brief Check if an identifier is a simple identifier, i.e., not a member expression, or a function param - * @param path - * 1. not a member expression - * 2. not a function param - * 3. not in a declaration - * 4. not as object property's not computed key - */ - private isStandAloneIdentifier(path: NodePath) { - const node = path.node; - const parentNode = path.parentPath?.node; - const isMemberExpression = this.t.isMemberExpression(parentNode) && parentNode.property === node; - if (isMemberExpression) return false; - const isFunctionParam = this.isAttrFromFunction(path, node.name); - if (isFunctionParam) return false; - while (path.parentPath) { - if (this.t.isVariableDeclarator(path.parentPath.node)) return false; - if ( - this.t.isObjectProperty(path.parentPath.node) && - path.parentPath.node.key === path.node && - !path.parentPath.node.computed - ) - return false; - path = path.parentPath as any; - } - return true; - } - - /** - * @brief Get all identifiers as strings in a node - * @param node - * @returns identifiers - */ - private getIdentifiers(node: t.Node): string[] { - if (this.t.isIdentifier(node)) return [node.name]; - const identifierKeys = new Set(); - this.traverse(this.valueWrapper(node as any), { - Identifier: innerPath => { - if (!this.isStandAloneIdentifier(innerPath)) return; - identifierKeys.add(innerPath.node.name); - }, - }); - return [...identifierKeys]; - } - - static escapeNamings = ['escape', '$']; - - /** - * @brief Check if it's the left side of an assignment expression, e.g. this.count = 1 - * @param innerPath - * @returns assignment expression - */ - isAssignmentExpressionLeft(innerPath: NodePath): NodePath | null { - let parentPath = innerPath.parentPath; - while (parentPath && !this.t.isStatement(parentPath.node)) { - if (this.t.isAssignmentExpression(parentPath.node)) { - if (parentPath.node.left === innerPath.node) return parentPath; - const leftPath = parentPath.get('left') as NodePath; - if (innerPath.isDescendant(leftPath)) return parentPath; - } else if (this.t.isUpdateExpression(parentPath.node)) { - return parentPath; - } - parentPath = parentPath.parentPath; - } - - return null; - } - - /** - * @brief Check if it's a reactivity function, e.g. arr.push - * @param innerPath - * @returns - */ - isAssignmentFunction(innerPath: NodePath): boolean { - let parentPath = innerPath.parentPath; - - while (parentPath && this.t.isMemberExpression(parentPath.node)) { - parentPath = parentPath.parentPath; - } - if (!parentPath) return false; - return ( - this.t.isCallExpression(parentPath.node) && - this.t.isMemberExpression(parentPath.node.callee) && - this.t.isIdentifier(parentPath.node.callee.property) && - reactivityFuncNames.includes(parentPath.node.callee.property.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 - */ - isMemberInEscapeFunction(innerPath: NodePath, stopNode: t.Node): boolean { - let isInFunction = false; - let reversePath = innerPath.parentPath; - while (reversePath && reversePath.node !== stopNode) { - const node = reversePath.node; - if ( - this.t.isCallExpression(node) && - this.t.isIdentifier(node.callee) && - PluginProvider.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 - */ - isMemberInManualFunction(innerPath: NodePath, stopNode: t.Node): boolean { - let isInFunction = false; - let reversePath = innerPath.parentPath; - while (reversePath && reversePath.node !== stopNode) { - const node = reversePath.node; - const parentNode = reversePath.parentPath?.node; - const isFunction = this.t.isFunctionExpression(node) || this.t.isArrowFunctionExpression(node); - const isManual = - this.t.isCallExpression(parentNode) && - this.t.isIdentifier(parentNode.callee) && - parentNode.callee.name === 'manual'; - if (isFunction && isManual) { - isInFunction = true; - break; - } - reversePath = reversePath.parentPath; - } - - return isInFunction; - } - - /** - * @brief Generate a random string - * @param length - * @returns random string - */ - private static uid(length = 4): string { - return Math.random() - .toString(32) - .slice(2, length + 2); - } -} - -/** - * @brief Change the PluginProvider class for class inheritance - */ -export let PluginProviderClass = PluginProvider; -export function changePluginProviderClass(cls: typeof PluginProvider) { - PluginProviderClass = cls; -} diff --git a/packages/transpiler/babel-preset-inula-next/src/types.ts b/packages/transpiler/babel-preset-inula-next/src/types.ts deleted file mode 100644 index a2e2049b..00000000 --- a/packages/transpiler/babel-preset-inula-next/src/types.ts +++ /dev/null @@ -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; -} - -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>; diff --git a/packages/transpiler/babel-preset-inula-next/test/condition.test.tsx b/packages/transpiler/babel-preset-inula-next/test/condition.test.tsx deleted file mode 100644 index 4e95cf02..00000000 --- a/packages/transpiler/babel-preset-inula-next/test/condition.test.tsx +++ /dev/null @@ -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
- 1}>{count} is bigger than is 1 - {count} is smaller than 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 "@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]; - } - }" - `); - }); -}); diff --git a/packages/transpiler/babel-preset-inula-next/test/earlyReturnAnanlyze.test.ts b/packages/transpiler/babel-preset-inula-next/test/earlyReturnAnanlyze.test.ts deleted file mode 100644 index aefca3df..00000000 --- a/packages/transpiler/babel-preset-inula-next/test/earlyReturnAnanlyze.test.ts +++ /dev/null @@ -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
1
- } - return
- 1}>{count} is bigger than is 1 - {count} is smaller than 1 -
; - } - `); - 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
1
- } - if (count > 2) { - return
2
- } - return
; - } - `); - 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
2
- } - return
1
- } - return
; - } - `); - 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); - }); -}); diff --git a/packages/transpiler/babel-preset-inula-next/test/index.test.tsx b/packages/transpiler/babel-preset-inula-next/test/index.test.tsx deleted file mode 100644 index b07ce5bc..00000000 --- a/packages/transpiler/babel-preset-inula-next/test/index.test.tsx +++ /dev/null @@ -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
- } - }`) - ).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
this.count++}>{this.count}
- } - }`) - ).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 <> -
- - } - }`) - ).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
count++}>{count}
- }`) - ).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 <> -

Hello dlight fn, {count}

- - -
{count}
- ; - }; - `) - ).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 <> - -
{this.count}
- ; - } - } - 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
{count}
; - } - `) - ).toMatchInlineSnapshot(` - "class CountComp extends View { - count = 0; - @Watch - _watch() { - if (this.count > 0) { - console.log(\`The count is greater than 0\`); - } - } - Body() { - return
{this.count}
; - } - } - export { CountComp as default };" - `); - }); - }); - - it('should transform function component reactively', () => { - expect( - transform(` - function MyComp() { - let count = 0 - return <> -

Hello dlight fn, {count}

- - -