diff --git a/packages/transpiler/babel-api/README.md b/packages/transpiler/babel-api/README.md new file mode 100644 index 00000000..ef06c4f4 --- /dev/null +++ b/packages/transpiler/babel-api/README.md @@ -0,0 +1,24 @@ +# @openinlua/babel-api +A package that encapsulates the babel API for use in the transpiler. + +To implement the dependency injection pattern, the package exports a function that registers the babel API in the +transpiler. + +```ts +import { registerBabelAPI } from '@openinlua/babel-api'; + +function plugin(api: typeof babel) { + registerBabelAPI(api); + + // Your babel plugin code here. +} +``` + +And then you can import to use it. +> types can use as a `type` or as a `namespace` for the babel API. + +```ts +import { types as t } from '@openinlua/babel-api'; + +t.isIdentifier(node as t.Node); +``` diff --git a/packages/transpiler/babel-api/package.json b/packages/transpiler/babel-api/package.json new file mode 100644 index 00000000..13bba1c1 --- /dev/null +++ b/packages/transpiler/babel-api/package.json @@ -0,0 +1,22 @@ +{ + "name": "@openinula/babel-api", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "src/index.mjs", + "typings": "src/index.d.ts", + "files": [ + "src" + ], + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@babel/core": "^7.23.3", + "@babel/types": "^7.24.0", + "@types/babel__core": "^7.20.5" + } +} diff --git a/packages/transpiler/babel-api/src/index.d.ts b/packages/transpiler/babel-api/src/index.d.ts new file mode 100644 index 00000000..b7ef6a5a --- /dev/null +++ b/packages/transpiler/babel-api/src/index.d.ts @@ -0,0 +1,7 @@ +import type babel from '@babel/core'; + +// use .d.ts to satisfy the type check +export * as types from '@babel/types'; + +export declare function register(api: typeof babel): void; +export declare function getBabelApi(): typeof babel; diff --git a/packages/transpiler/babel-inula-next-core/src/babelTypes.ts b/packages/transpiler/babel-api/src/index.mjs similarity index 58% rename from packages/transpiler/babel-inula-next-core/src/babelTypes.ts rename to packages/transpiler/babel-api/src/index.mjs index bb24c43d..fbb81844 100644 --- a/packages/transpiler/babel-inula-next-core/src/babelTypes.ts +++ b/packages/transpiler/babel-api/src/index.mjs @@ -1,13 +1,20 @@ -import { type types as t } from '@babel/core'; -import type babel from '@babel/core'; -let _t: null | typeof types = null; -let babelApi: typeof babel | null = null; -export const register = (api: typeof babel) => { +/** @type {null | typeof import('@babel/core').types} */ +let _t = null; +/** @type {null | typeof import('@babel/core')} */ +let babelApi = null; + +/** + * @param {import('@babel/core')} api + */ +export const register = api => { babelApi = api; _t = api.types; }; -export const getBabelApi = (): typeof babel => { +/** + * @returns {typeof import('@babel/core')} + */ +export const getBabelApi = () => { if (!babelApi) { throw new Error('Please call register() before using the babel api'); } @@ -28,4 +35,4 @@ export const types = new Proxy( return undefined; }, } -) as typeof t; +); diff --git a/packages/transpiler/babel-inula-next-core/package.json b/packages/transpiler/babel-inula-next-core/package.json index a7b33eec..12124df1 100644 --- a/packages/transpiler/babel-inula-next-core/package.json +++ b/packages/transpiler/babel-inula-next-core/package.json @@ -44,7 +44,8 @@ "@types/babel__generator": "^7.6.8", "@types/babel__parser": "^7.1.1", "@types/babel__traverse": "^7.6.8", - "jsx-view-parser": "workspace:*", + "@openinula/jsx-view-parser": "workspace:*", + "@openinula/babel-api": "workspace:*", "minimatch": "^9.0.3", "vitest": "^1.4.0" }, diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/functionalMacroAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts similarity index 94% rename from packages/transpiler/babel-inula-next-core/src/analyze/functionalMacroAnalyze.ts rename to packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts index cd428a71..6b9e024f 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/functionalMacroAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts @@ -16,7 +16,7 @@ import { NodePath } from '@babel/core'; import { LifeCycle, Visitor } from './types'; import { addLifecycle, addWatch } from './nodeFactory'; -import * as t from '@babel/types'; +import { types as t } from '@openinula/babel-api'; import { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../constants'; import { extractFnFromMacro, getFnBody } from '../utils'; @@ -51,6 +51,9 @@ export function functionalMacroAnalyze(): Visitor { if (calleeName === WATCH) { const fnNode = extractFnFromMacro(expression, WATCH); const deps = getWatchDeps(expression); + if (!deps) { + // we auto collect the deps from the function body + } addWatch(ctx.current, fnNode, deps); return; } diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/hookAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/hookAnalyze.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/index.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/index.ts similarity index 82% rename from packages/transpiler/babel-inula-next-core/src/analyze/index.ts rename to packages/transpiler/babel-inula-next-core/src/analyzer/index.ts index 5374263d..b183af2b 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/index.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/index.ts @@ -1,18 +1,14 @@ -import { type types as t, type NodePath } from '@babel/core'; +import { type NodePath } from '@babel/core'; import { propsAnalyze } from './propsAnalyze'; -import { AnalyzeContext, Analyzer, ComponentNode, CondNode, Visitor } from './types'; +import { AnalyzeContext, Analyzer, ComponentNode, Visitor } from './types'; import { addLifecycle, createComponentNode } from './nodeFactory'; -import { propertiesAnalyze } from './propertiesAnalyze'; +import { variablesAnalyze } from './variablesAnalyze'; import { functionalMacroAnalyze } from './functionalMacroAnalyze'; import { getFnBody } from '../utils'; import { viewAnalyze } from './viewAnalyze'; import { WILL_MOUNT } from '../constants'; -import { types } from '../babelTypes'; -const builtinAnalyzers = [propsAnalyze, propertiesAnalyze, functionalMacroAnalyze, viewAnalyze]; - -export function isCondNode(node: any): node is CondNode { - return node && node.type === 'cond'; -} +import { types as t } from '@openinula/babel-api'; +const builtinAnalyzers = [propsAnalyze, variablesAnalyze, functionalMacroAnalyze, viewAnalyze]; function mergeVisitor(...visitors: Analyzer[]): Visitor { return visitors.reduce>((acc, cur) => { @@ -75,7 +71,7 @@ export function analyzeFnComp( } if (context.unhandledNode.length) { - addLifecycle(componentNode, WILL_MOUNT, types.blockStatement(context.unhandledNode)); + addLifecycle(componentNode, WILL_MOUNT, t.blockStatement(context.unhandledNode)); } } /** @@ -85,10 +81,9 @@ export function analyzeFnComp( * 2. identify the component's props, including children, alias, and default value * 3. analyze the early return of the component, build into the branch * - * @param types * @param fnName * @param path - * @param customAnalyzers + * @param options */ export function analyze( fnName: string, diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/nodeFactory.ts similarity index 70% rename from packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts rename to packages/transpiler/babel-inula-next-core/src/analyzer/nodeFactory.ts index 289ba84b..0678f030 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/nodeFactory.ts @@ -14,9 +14,9 @@ */ import { NodePath, type types as t } from '@babel/core'; -import { ComponentNode, FunctionalExpression, LifeCycle, ViewNode } from './types'; +import { ComponentNode, FunctionalExpression, LifeCycle, ReactiveVariable } from './types'; import { PropType } from '../constants'; -import { ViewParticle } from '@openinula/reactivity-parser'; +import { ViewParticle, PrevMap } from '@openinula/reactivity-parser'; export function createComponentNode( name: string, @@ -25,36 +25,32 @@ export function createComponentNode( ): ComponentNode { const comp: ComponentNode = { type: 'comp', + level: parent ? parent.level + 1 : 0, name, - props: [], - child: undefined, + children: undefined, variables: [], - dependencyMap: {}, - reactiveMap: {}, + dependencyMap: parent ? { [PrevMap]: parent.dependencyMap } : {}, lifecycle: {}, parent, fnNode, - get availableProps() { - return comp.props - .map(({ name, nestedProps, alias }) => { - const nested = nestedProps ? nestedProps.map(name => name) : []; - return [alias ? alias : name, ...nested]; - }) - .flat(); - }, get ownAvailableVariables() { - return [...comp.variables.filter(p => p.type === 'reactive').map(({ name }) => name), ...comp.availableProps]; + return [...comp.variables.filter((p): p is ReactiveVariable => p.type === 'reactive')]; }, get availableVariables() { - return [...comp.ownAvailableVariables, ...(comp.parent ? comp.parent.availableVariables : [])]; + // Here is critical for the dependency analysis, must put parent's availableVariables first + // so the subcomponent can react to the parent's variables change + return [...(comp.parent ? comp.parent.availableVariables : []), ...comp.ownAvailableVariables]; }, }; return comp; } -export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, isComputed: boolean) { - comp.variables.push({ name, value, isComputed, type: 'reactive' }); +export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, deps: string[] | null) { + comp.variables.push({ name, value, isComputed: !!deps?.length, type: 'reactive', deps }); + if (comp.dependencyMap[name] === undefined) { + comp.dependencyMap[name] = null; + } } export function addMethod(comp: ComponentNode, name: string, value: FunctionalExpression) { @@ -98,9 +94,7 @@ export function addWatch( } export function setViewChild(comp: ComponentNode, view: ViewParticle[], usedPropertySet: Set) { - const viewNode: ViewNode = { - content: view, - usedPropertySet, - }; - comp.child = viewNode; + // TODO: Maybe we should merge + comp.usedPropertySet = usedPropertySet; + comp.children = view; } diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/propsAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/propsAnalyze.ts similarity index 84% rename from packages/transpiler/babel-inula-next-core/src/analyze/propsAnalyze.ts rename to packages/transpiler/babel-inula-next-core/src/analyzer/propsAnalyze.ts index e6d1a448..a65373db 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/propsAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/propsAnalyze.ts @@ -1,8 +1,17 @@ -import { type types as t, type NodePath } from '@babel/core'; +import { type NodePath } from '@babel/core'; import { AnalyzeContext, Visitor } from './types'; import { addProp } from './nodeFactory'; import { PropType } from '../constants'; -import { types } from '../babelTypes'; +import { types as t } from '@openinula/babel-api'; + +export interface Prop { + name: string; + type: PropType; + alias: string | null; + default: t.Expression | null; + nestedProps: string[] | null; + nestedRelationship: t.ObjectPattern | t.ArrayPattern | null; +} /** * Analyze the props deconstructing in the function component @@ -29,8 +38,8 @@ export function propsAnalyze(): Visitor { // --- normal property --- const key = path.node.key; const value = path.node.value; - if (types.isIdentifier(key) || types.isStringLiteral(key)) { - const name = types.isIdentifier(key) ? key.name : key.value; + if (t.isIdentifier(key) || t.isStringLiteral(key)) { + const name = t.isIdentifier(key) ? key.name : key.value; analyzeSingleProp(value, name, path, ctx); return; } @@ -57,17 +66,17 @@ function analyzeSingleProp( let alias: string | null = null; const nestedProps: string[] | null = []; let nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null; - if (types.isIdentifier(value)) { + if (t.isIdentifier(value)) { // 1. handle alias without default value // handle alias without default value if (key !== value.name) { alias = value.name; } - } else if (types.isAssignmentPattern(value)) { + } else if (t.isAssignmentPattern(value)) { // 2. handle default value case const assignedName = value.left; defaultVal = value.right; - if (types.isIdentifier(assignedName)) { + if (t.isIdentifier(assignedName)) { if (assignedName.name !== key) { // handle alias in default value case alias = assignedName.name; @@ -75,7 +84,7 @@ function analyzeSingleProp( } else { throw Error(`Unsupported assignment type in object destructuring: ${assignedName.type}`); } - } else if (types.isObjectPattern(value) || types.isArrayPattern(value)) { + } else if (t.isObjectPattern(value) || t.isArrayPattern(value)) { // 3. nested destructuring // we should collect the identifier that can be used in the function body as the prop // e.g. function ({prop1, prop2: [p20X, {p211, p212: p212X}]} 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 new file mode 100644 index 00000000..43d4223c --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/reactive/getDependencies.ts @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import type { NodePath } from '@babel/core'; +import { AnalyzeContext, DependencyMap } from '../types'; +import { types as t } from '@openinula/babel-api'; +import { reactivityFuncNames } from '../../const'; +import { PrevMap } from '@openinula/reactivity-parser'; + +/** + * @brief Get all valid dependencies of a babel path + * @param propertyKey + * @param path + * @param ctx + * @returns + */ +export function getDependenciesFromNode( + propertyKey: string, + path: NodePath, + { current }: AnalyzeContext +) { + // ---- Deps: console.log(count) + const deps = new Set(); + // ---- Assign deps: count = 1 or count++ + const assignDeps = new Set(); + const depNodes: Record = {}; + + const visitor = (innerPath: NodePath) => { + const propertyKey = innerPath.node.name; + if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) { + assignDeps.add(propertyKey); + } else if (current.availableVariables.includes(propertyKey)) { + deps.add(propertyKey); + findDependency(current.dependencyMap, propertyKey)?.forEach(deps.add.bind(deps)); + if (!depNodes[propertyKey]) depNodes[propertyKey] = []; + depNodes[propertyKey].push(t.cloneNode(innerPath.node)); + } + }; + if (path.isIdentifier()) { + visitor(path); + } + path.traverse({ + Identifier: visitor, + }); + + // ---- Eliminate deps that are assigned in the same method + // e.g. { console.log(count); count = 1 } + // this will cause infinite loop + // so we eliminate "count" from deps + assignDeps.forEach(dep => { + deps.delete(dep); + }); + + const depArr = [...deps]; + if (deps.size > 0) { + current.dependencyMap[propertyKey] = depArr; + } + + return depArr; +} + +/** + * @brief Check if it's the left side of an assignment expression, e.g. count = 1 + * @param innerPath + * @returns assignment expression + */ +function isAssignmentExpressionLeft(innerPath: NodePath): NodePath | null { + let parentPath = innerPath.parentPath; + while (parentPath && !parentPath.isStatement()) { + if (parentPath.isAssignmentExpression()) { + if (parentPath.node.left === innerPath.node) return parentPath; + const leftPath = parentPath.get('left') as NodePath; + if (innerPath.isDescendant(leftPath)) return parentPath; + } else if (parentPath.isUpdateExpression()) { + return parentPath; + } + parentPath = parentPath.parentPath; + } + + return null; +} + +/** + * @brief Check if it's a reactivity function, e.g. arr.push + * @param innerPath + * @returns + */ +function isAssignmentFunction(innerPath: NodePath): boolean { + let parentPath = innerPath.parentPath; + + while (parentPath && parentPath.isMemberExpression()) { + parentPath = parentPath.parentPath; + } + if (!parentPath) return false; + return ( + parentPath.isCallExpression() && + parentPath.get('callee').isIdentifier() && + reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name) + ); +} + +function findDependency(dependencyMap: DependencyMap, propertyKey: string) { + let currentMap: DependencyMap | undefined = dependencyMap; + do { + if (currentMap[propertyKey] !== undefined) { + return currentMap[propertyKey]; + } + // trace back to the previous map + currentMap = currentMap[PrevMap]; + } while (currentMap); + return null; +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/types.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/types.ts similarity index 85% rename from packages/transpiler/babel-inula-next-core/src/analyze/types.ts rename to packages/transpiler/babel-inula-next-core/src/analyzer/types.ts index 56bd4b1d..7a6a8a77 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/types.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/types.ts @@ -15,20 +15,21 @@ 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 { ViewParticle, PrevMap } from '@openinula/reactivity-parser'; export type LifeCycle = typeof WILL_MOUNT | typeof ON_MOUNT | typeof WILL_UNMOUNT | typeof ON_UNMOUNT; type Bitmap = number; export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression; + interface BaseVariable { name: string; value: V; } + export interface ReactiveVariable extends BaseVariable { type: 'reactive'; - // indicate the value is a state or computed or watch - listeners?: string[]; + level: number; bitmap?: Bitmap; // need a flag for computed to gen a getter // watch is a static computed @@ -38,11 +39,13 @@ export interface ReactiveVariable extends BaseVariable { export interface MethodVariable extends BaseVariable { type: 'method'; } + export interface SubCompVariable extends BaseVariable { type: 'subComp'; } export type Variable = ReactiveVariable | MethodVariable | SubCompVariable; + export interface Prop { name: string; type: PropType; @@ -51,12 +54,17 @@ export interface Prop { nestedProps: string[] | null; nestedRelationship: t.ObjectPattern | t.ArrayPattern | null; } + export interface ComponentNode { type: 'comp'; name: string; - props: Prop[]; + level: number; // The variables defined in the component variables: Variable[]; + /** + * The used properties in the component + */ + usedPropertySet?: Set; /** * The available props for the component, including the nested props */ @@ -64,37 +72,27 @@ export interface ComponentNode { /** * The available variables and props owned by the component */ - ownAvailableVariables: string[]; + ownAvailableVariables: ReactiveVariable[]; /** * The available variables and props for the component and its parent */ - availableVariables: string[]; - /** - * The map to find the dependencies - */ - dependencyMap: { - [key: string]: string[]; - }; - child?: ComponentNode | ViewNode; + availableVariables: ReactiveVariable[]; + children?: (ComponentNode | ViewParticle)[]; parent?: ComponentNode; /** * The function body of the fn component code */ fnNode: NodePath; - /** - * The map to find the state - */ - reactiveMap: Record; lifecycle: Partial>; + /** + * The watch fn in the component + */ watch?: { + bit: Bitmap; deps: NodePath | null; callback: NodePath | NodePath; }[]; } -export interface ViewNode { - content: ViewParticle[]; - usedPropertySet: Set; -} export interface AnalyzeContext { level: number; diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/utils.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/utils.ts similarity index 100% rename from packages/transpiler/babel-inula-next-core/src/analyze/utils.ts rename to packages/transpiler/babel-inula-next-core/src/analyzer/utils.ts diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/propertiesAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/variablesAnalyze.ts similarity index 52% rename from packages/transpiler/babel-inula-next-core/src/analyze/propertiesAnalyze.ts rename to packages/transpiler/babel-inula-next-core/src/analyzer/variablesAnalyze.ts index 083548c1..b5fadde7 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/propertiesAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/variablesAnalyze.ts @@ -13,21 +13,21 @@ * See the Mulan PSL v2 for more details. */ -import { AnalyzeContext, Visitor } from './types'; +import { Visitor } from './types'; import { addMethod, addProperty, addSubComponent, createComponentNode } from './nodeFactory'; import { isValidPath } from './utils'; -import { type types as t, type NodePath } from '@babel/core'; -import { reactivityFuncNames } from '../const'; -import { types } from '../babelTypes'; +import { type NodePath } from '@babel/core'; import { COMPONENT } from '../constants'; import { analyzeFnComp } from '.'; +import { getDependenciesFromNode } from './reactive/getDependencies'; +import { types as t } from '@openinula/babel-api'; /** * collect all properties and methods from the node * and analyze the dependencies of the properties * @returns */ -export function propertiesAnalyze(): Visitor { +export function variablesAnalyze(): Visitor { return { VariableDeclaration(path: NodePath, ctx) { const declarations = path.get('declarations'); @@ -41,7 +41,7 @@ export function propertiesAnalyze(): Visitor { // TODO: handle array destructuring throw new Error('Array destructuring is not supported yet'); } else if (id.isIdentifier()) { - // --- properties: the state / computed / plain properties / methods--- + // --- properties: the state / computed / plain properties / methods --- const init = declaration.get('init'); let deps: string[] | null = null; if (isValidPath(init)) { @@ -50,7 +50,7 @@ export function propertiesAnalyze(): Visitor { addMethod(ctx.current, id.node.name, init.node); return; } - // handle the sub component + // handle the subcomponent // Should like Component(() => {}) if ( init.isCallExpression() && @@ -64,14 +64,13 @@ export function propertiesAnalyze(): Visitor { const subComponent = createComponentNode(id.node.name, fnNode, ctx.current); analyzeFnComp(fnNode, subComponent, ctx); - deps = getDependenciesFromNode(id.node.name, init, ctx); addSubComponent(ctx.current, subComponent); return; } deps = getDependenciesFromNode(id.node.name, init, ctx); } - addProperty(ctx.current, id.node.name, init.node || null, !!deps?.length); + addProperty(ctx.current, id.node.name, init.node || null, deps); } }); }, @@ -81,7 +80,7 @@ export function propertiesAnalyze(): Visitor { throw new Error('Function declaration must have an id'); } - const functionExpression = types.functionExpression( + const functionExpression = t.functionExpression( path.node.id, path.node.params, path.node.body, @@ -92,90 +91,3 @@ export function propertiesAnalyze(): Visitor { }, }; } - -/** - * @brief Get all valid dependencies of a babel path - * @param propertyKey - * @param path - * @param ctx - * @returns - */ -function getDependenciesFromNode( - propertyKey: string, - path: NodePath, - { current }: AnalyzeContext -) { - // ---- Deps: console.log(this.count) - const deps = new Set(); - // ---- Assign deps: this.count = 1 / this.count++ - const assignDeps = new Set(); - const visitor = (innerPath: NodePath) => { - const propertyKey = innerPath.node.name; - if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) { - assignDeps.add(propertyKey); - } else if (current.availableVariables.includes(propertyKey)) { - deps.add(propertyKey); - } - }; - if (path.isIdentifier()) { - visitor(path); - } - path.traverse({ - Identifier: visitor, - }); - - // ---- Eliminate deps that are assigned in the same method - // e.g. { console.log(this.count); this.count = 1 } - // this will cause infinite loop - // so we eliminate "count" from deps - assignDeps.forEach(dep => { - deps.delete(dep); - }); - - const depArr = [...deps]; - if (deps.size > 0) { - current.dependencyMap[propertyKey] = depArr; - } - - return depArr; -} - -/** - * @brief Check if it's the left side of an assignment expression, e.g. count = 1 - * @param innerPath - * @returns assignment expression - */ -function isAssignmentExpressionLeft(innerPath: NodePath): NodePath | null { - let parentPath = innerPath.parentPath; - while (parentPath && !parentPath.isStatement()) { - if (parentPath.isAssignmentExpression()) { - if (parentPath.node.left === innerPath.node) return parentPath; - const leftPath = parentPath.get('left') as NodePath; - if (innerPath.isDescendant(leftPath)) return parentPath; - } else if (parentPath.isUpdateExpression()) { - return parentPath; - } - parentPath = parentPath.parentPath; - } - - return null; -} - -/** - * @brief Check if it's a reactivity function, e.g. arr.push - * @param innerPath - * @returns - */ -function isAssignmentFunction(innerPath: NodePath): boolean { - let parentPath = innerPath.parentPath; - - while (parentPath && parentPath.isMemberExpression()) { - parentPath = parentPath.parentPath; - } - if (!parentPath) return false; - return ( - parentPath.isCallExpression() && - parentPath.get('callee').isIdentifier() && - reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name) - ); -} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/viewAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/viewAnalyze.ts similarity index 89% rename from packages/transpiler/babel-inula-next-core/src/analyze/viewAnalyze.ts rename to packages/transpiler/babel-inula-next-core/src/analyzer/viewAnalyze.ts index 24a59271..bae240a3 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/viewAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/viewAnalyze.ts @@ -14,9 +14,9 @@ */ import { Visitor } from './types'; -import { type types as t, type NodePath } from '@babel/core'; -import { parseView as parseJSX } from 'jsx-view-parser'; -import { getBabelApi } from '../babelTypes'; +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'; diff --git a/packages/transpiler/babel-inula-next-core/src/constants.ts b/packages/transpiler/babel-inula-next-core/src/constants.ts index 6556fd3f..53ddcbca 100644 --- a/packages/transpiler/babel-inula-next-core/src/constants.ts +++ b/packages/transpiler/babel-inula-next-core/src/constants.ts @@ -8,7 +8,6 @@ export enum PropType { REST = 'rest', SINGLE = 'single', } - export const reactivityFuncNames = [ // ---- Array 'push', diff --git a/packages/transpiler/babel-inula-next-core/src/generator/index.ts b/packages/transpiler/babel-inula-next-core/src/generator/index.ts new file mode 100644 index 00000000..c7551d26 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/src/generator/index.ts @@ -0,0 +1,94 @@ +import { ViewParticle } from '@openinula/reactivity-parser'; +import { ComponentNode, Prop, Variable } from '../analyzer/types'; +import { type types as t, type NodePath } from '@babel/core'; +import { types } from '../babelTypes'; + +type Visitor = { + [Type in (ViewParticle | ComponentNode)['type']]: ( + node: Extract, + 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 c50d8feb..07bdc408 100644 --- a/packages/transpiler/babel-inula-next-core/src/index.ts +++ b/packages/transpiler/babel-inula-next-core/src/index.ts @@ -1,7 +1,7 @@ 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 inulaNext from './plugin'; import { type DLightOption } from './types'; import { type ConfigAPI, type TransformOptions } from '@babel/core'; import { plugin as fn2Class } from '@openinula/class-transformer'; @@ -14,7 +14,7 @@ export default function (_: ConfigAPI, options: DLightOption): TransformOptions [syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }], [syntaxDecorators.default ?? syntaxDecorators, { legacy: true }], fn2Class, - [dlight, options], + [inulaNext, options], ], }; } diff --git a/packages/transpiler/babel-inula-next-core/src/main.ts b/packages/transpiler/babel-inula-next-core/src/main.ts deleted file mode 100644 index f01485bd..00000000 --- a/packages/transpiler/babel-inula-next-core/src/main.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type babel from '@babel/core'; -import { type PluginObj } from '@babel/core'; -import { type DLightOption } from './types'; -import { defaultAttributeMap, defaultHTMLTags } from './const'; -import { analyze } from './analyze'; -import { NodePath, type types as t } from '@babel/core'; -import { COMPONENT } from './constants'; -import { extractFnFromMacro } from './utils'; -import { register } from './babelTypes'; - -export default function (api: typeof babel, options: DLightOption): PluginObj { - const { types } = api; - const { - files = '**/*.{js,ts,jsx,tsx}', - excludeFiles = '**/{dist,node_modules,lib}/*', - enableDevTools = false, - customHtmlTags = defaultHtmlTags => defaultHtmlTags, - attributeMap = defaultAttributeMap, - } = options; - - const htmlTags = - typeof customHtmlTags === 'function' - ? customHtmlTags(defaultHTMLTags) - : customHtmlTags.includes('*') - ? [...new Set([...defaultHTMLTags, ...customHtmlTags])].filter(tag => tag !== '*') - : customHtmlTags; - - register(api); - return { - visitor: { - Program: { - enter(path, { filename }) { - // return pluginProvider.programEnterVisitor(path, filename); - }, - exit(path, { filename }) { - // pluginProvider.programExitVisitor.bind(pluginProvider); - }, - }, - CallExpression(path: NodePath) { - // find the component, like: Component(() => {}) - const callee = path.get('callee'); - - if (callee.isIdentifier() && callee.node.name === COMPONENT) { - const componentNode = extractFnFromMacro(path, COMPONENT); - let name = ''; - // try to get the component name, when parent is a variable declarator - if (path.parentPath.isVariableDeclarator()) { - const lVal = path.parentPath.get('id'); - if (lVal.isIdentifier()) { - name = lVal.node.name; - } else { - console.error('Component macro must be assigned to a variable'); - } - } - const root = analyze(name, componentNode, { - htmlTags, - }); - // The sub path has been visited, so we just skip - path.skip(); - } - }, - }, - }; -} diff --git a/packages/transpiler/babel-inula-next-core/src/plugin.ts b/packages/transpiler/babel-inula-next-core/src/plugin.ts index e5778cd5..62d5c493 100644 --- a/packages/transpiler/babel-inula-next-core/src/plugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/plugin.ts @@ -1,8 +1,12 @@ 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 { defaultAttributeMap, defaultHTMLTags } from './const'; +import { analyze } from './analyzer'; +import { NodePath, type types as t } from '@babel/core'; +import { COMPONENT } from './constants'; +import { extractFnFromMacro } from './utils'; +import { register } from '@openinula/babel-api'; export default function (api: typeof babel, options: DLightOption): PluginObj { const { types } = api; @@ -10,34 +14,51 @@ export default function (api: typeof babel, options: DLightOption): PluginObj { files = '**/*.{js,ts,jsx,tsx}', excludeFiles = '**/{dist,node_modules,lib}/*', enableDevTools = false, - htmlTags = defaultHtmlTags => defaultHtmlTags, + htmlTags: customHtmlTags = defaultHtmlTags => defaultHtmlTags, attributeMap = defaultAttributeMap, } = options; - const pluginProvider = new PluginProviderClass( - api, - types, - Array.isArray(files) ? files : [files], - Array.isArray(excludeFiles) ? excludeFiles : [excludeFiles], - enableDevTools, - htmlTags, - attributeMap - ); + const htmlTags = + typeof customHtmlTags === 'function' + ? customHtmlTags(defaultHTMLTags) + : customHtmlTags.includes('*') + ? [...new Set([...defaultHTMLTags, ...customHtmlTags])].filter(tag => tag !== '*') + : customHtmlTags; + register(api); return { visitor: { Program: { enter(path, { filename }) { - return pluginProvider.programEnterVisitor(path, filename); + // return pluginProvider.programEnterVisitor(path, filename); + }, + exit(path, { filename }) { + // pluginProvider.programExitVisitor.bind(pluginProvider); }, - exit: pluginProvider.programExitVisitor.bind(pluginProvider), }, - ClassDeclaration: { - enter: pluginProvider.classEnter.bind(pluginProvider), - exit: pluginProvider.classExit.bind(pluginProvider), + CallExpression(path: NodePath) { + // find the component, like: Component(() => {}) + const callee = path.get('callee'); + + if (callee.isIdentifier() && callee.node.name === COMPONENT) { + const componentNode = extractFnFromMacro(path, COMPONENT); + let name = ''; + // try to get the component name, when parent is a variable declarator + if (path.parentPath.isVariableDeclarator()) { + const lVal = path.parentPath.get('id'); + if (lVal.isIdentifier()) { + name = lVal.node.name; + } else { + console.error('Component macro must be assigned to a variable'); + } + } + const root = analyze(name, componentNode, { + htmlTags, + }); + // The sub path has been visited, so we just skip + path.skip(); + } }, - ClassMethod: pluginProvider.classMethodVisitor.bind(pluginProvider), - ClassProperty: pluginProvider.classPropertyVisitor.bind(pluginProvider), }, }; } diff --git a/packages/transpiler/babel-inula-next-core/src/pluginProvider.ts b/packages/transpiler/babel-inula-next-core/src/pluginProvider.ts deleted file mode 100644 index 386fec16..00000000 --- a/packages/transpiler/babel-inula-next-core/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 '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-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts index 482554a8..10ac4ad3 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts @@ -25,7 +25,7 @@ export function earlyReturnPlugin(): Visitor { const argument = path.get('argument'); if (argument.isJSXElement()) { - currentComp.child = createJSXNode(currentComp, argument); + currentComp.children = createJSXNode(currentComp, argument); } }, IfStatement(ifStmt: NodePath, context: AnalyzeContext) { @@ -63,7 +63,7 @@ export function earlyReturnPlugin(): Visitor { ); context.skipRest(); - currentComp.child = createCondNode(currentComp, defaultComponent, branches); + currentComp.children = createCondNode(currentComp, defaultComponent, branches); }, }; } 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 62c28ba0..b7b33319 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,12 +15,11 @@ import { describe, expect, it } from 'vitest'; import { genCode, mockAnalyze } from '../mock'; -import { functionalMacroAnalyze } from '../../src/analyze/functionalMacroAnalyze'; -import { types } from '../../src/babelTypes'; -import { type NodePath, type types as t } from '@babel/core'; +import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze'; +import { types as t } from '@openinula/babel-api'; const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]); -const combine = (body: t.Statement[]) => types.program(body); +const combine = (body: t.Statement[]) => t.program(body); describe('analyze lifeCycle', () => { it('should collect will mount', () => { 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 c0704ae6..ae1648b9 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,11 +15,11 @@ import { describe, expect, it } from 'vitest'; import { genCode, mockAnalyze } from '../mock'; -import { propertiesAnalyze } from '../../src/analyze/propertiesAnalyze'; -import { propsAnalyze } from '../../src/analyze/propsAnalyze'; -import { ComponentNode } from '../../src/analyze/types'; +import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze'; +import { propsAnalyze } from '../../src/analyzer/propsAnalyze'; +import { ComponentNode, ReactiveVariable } from '../../src/analyzer/types'; -const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, propertiesAnalyze]); +const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, variablesAnalyze]); describe('analyze properties', () => { it('should work', () => { @@ -41,11 +41,14 @@ describe('analyze properties', () => { }) `); expect(root.variables.length).toBe(2); - expect(root.variables[0].isComputed).toBe(false); - expect(genCode(root.variables[0].value)).toBe('1'); - expect(root.variables[1].isComputed).toBe(true); - expect(genCode(root.variables[1].value)).toBe('foo'); - expect(root.dependencyMap).toEqual({ bar: ['foo'] }); + const fooVar = root.variables[0] as ReactiveVariable; + expect(fooVar.isComputed).toBe(false); + expect(genCode(fooVar.value)).toBe('1'); + + const barVar = root.variables[1] as ReactiveVariable; + expect(barVar.isComputed).toBe(true); + expect(genCode(barVar.value)).toBe('foo'); + expect(root.dependencyMap).toEqual({ bar: ['foo'], foo: null }); }); it('should analyze dependency from state in different shape', () => { @@ -58,13 +61,15 @@ describe('analyze properties', () => { }) `); expect(root.variables.length).toBe(4); - expect(root.variables[3].isComputed).toBe(true); - expect(genCode(root.variables[3].value)).toMatchInlineSnapshot(` + + const barVar = root.variables[3] as ReactiveVariable; + expect(barVar.isComputed).toBe(true); + expect(genCode(barVar.value)).toMatchInlineSnapshot(` "{ foo: foo ? a : b }" `); - expect(root.dependencyMap).toEqual({ bar: ['foo', 'a', 'b'] }); + expect(root.dependencyMap).toEqual({ bar: ['foo', 'a', 'b'], foo: null, a: null, b: null }); }); it('should analyze dependency from props', () => { @@ -74,7 +79,9 @@ describe('analyze properties', () => { }) `); expect(root.variables.length).toBe(1); - expect(root.variables[0].isComputed).toBe(true); + + const barVar = root.variables[0] as ReactiveVariable; + expect(barVar.isComputed).toBe(true); expect(root.dependencyMap).toEqual({ bar: ['foo'] }); }); @@ -85,7 +92,8 @@ describe('analyze properties', () => { }) `); expect(root.variables.length).toBe(1); - expect(root.variables[0].isComputed).toBe(true); + const barVar = root.variables[0] as ReactiveVariable; + expect(barVar.isComputed).toBe(true); expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] }); }); @@ -97,8 +105,9 @@ describe('analyze properties', () => { }) `); expect(root.variables.length).toBe(1); - expect(root.variables[0].isComputed).toBe(false); - expect(root.dependencyMap).toEqual({}); + const barVar = root.variables[0] as ReactiveVariable; + expect(barVar.isComputed).toBe(false); + expect(root.dependencyMap).toEqual({ bar: null }); }); }); @@ -113,15 +122,90 @@ describe('analyze properties', () => { }) `); expect(root.variables.length).toBe(2); - expect(root.dependencyMap).toEqual({ Sub: ['foo'] }); + expect(root.dependencyMap).toEqual({ foo: null }); expect((root.variables[1].value as ComponentNode).dependencyMap).toMatchInlineSnapshot(` { "bar": [ "foo", ], + Symbol(prevMap): { + "foo": null, + }, } `); }); + + it('should analyze dependency in parent', () => { + const root = analyze(` + Component(({lastName}) => { + let parentFirstName = 'sheldon'; + const parentName = parentFirstName + lastName; + const Son = Component(() => { + let middleName = parentName + const name = 'shelly'+ middleName + lastName; + const GrandSon = Component(() => { + let grandSonName = 'bar' + lastName; + }); + }); + }) + `); + const sonNode = root.variables[2].value as ComponentNode; + expect(sonNode.dependencyMap).toMatchInlineSnapshot(` + { + "middleName": [ + "parentName", + "parentFirstName", + "lastName", + ], + "name": [ + "middleName", + "parentName", + "parentFirstName", + "lastName", + ], + Symbol(prevMap): { + "parentFirstName": null, + "parentName": [ + "parentFirstName", + "lastName", + ], + }, + } + `); + const grandSonNode = sonNode.variables[2].value as ComponentNode; + expect(grandSonNode.dependencyMap).toMatchInlineSnapshot(` + { + "grandSonName": [ + "lastName", + ], + Symbol(prevMap): { + "middleName": [ + "parentName", + "parentFirstName", + "lastName", + ], + "name": [ + "middleName", + "parentName", + "parentFirstName", + "lastName", + ], + Symbol(prevMap): { + "parentFirstName": null, + "parentName": [ + "parentFirstName", + "lastName", + ], + }, + }, + } + `); + }); + // SubscriptionTree + // const SubscriptionTree = { + // lastName: ['parentName','son:middleName','son:name','son,grandSon:grandSonName'], + // + // } }); it('should collect method', () => { @@ -138,6 +222,10 @@ describe('analyze properties', () => { expect(root.variables.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']); expect(root.variables[1].type).toBe('method'); expect(root.variables[2].type).toBe('method'); - expect(root.dependencyMap).toMatchInlineSnapshot('{}'); + expect(root.dependencyMap).toMatchInlineSnapshot(` + { + "foo": null, + } + `); }); }); diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/props.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/props.test.ts index 85231b97..1caaab8a 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/props.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/props.test.ts @@ -16,7 +16,7 @@ import { describe, expect, it } from 'vitest'; import { genCode, mockAnalyze } from '../mock'; import { PropType } from '../../src/constants'; -import { propsAnalyze } from '../../src/analyze/propsAnalyze'; +import { propsAnalyze } from '../../src/analyzer/propsAnalyze'; const analyze = (code: string) => mockAnalyze(code, [propsAnalyze]); 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 23698fa7..d833b187 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,19 +1,82 @@ -import { propertiesAnalyze } from '../../src/analyze/propertiesAnalyze'; -import { propsAnalyze } from '../../src/analyze/propsAnalyze'; -import { viewAnalyze } from '../../src/analyze/viewAnalyze'; +import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze'; +import { propsAnalyze } from '../../src/analyzer/propsAnalyze'; +import { ComponentNode } from '../../src/analyzer/types'; +import { viewAnalyze } from '../../src/analyzer/viewAnalyze'; import { genCode, mockAnalyze } from '../mock'; import { describe, expect, it } from 'vitest'; -const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, propertiesAnalyze, viewAnalyze]); - -describe('watchAnalyze', () => { - it('should analyze watch expressions', () => { +const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, variablesAnalyze, viewAnalyze]); +describe('viewAnalyze', () => { + it('should analyze view', () => { const root = analyze(/*js*/ ` - Comp(({name}) => { - let count = 11 - return
{count}
- }) + Component(({name ,className}) => { + let count = name; // 1 + let doubleCount = count* 2; // 2 + let doubleCount2 = doubleCount* 2; // 4 + const Input = Component(() => { + let count = 1; + watch(() => { + if (doubleCount2 > 10) { + count++; + } + console.log(doubleCount2); + }); + const update = changed => { + if (changed & 0x1011) { + node1.update(_$this0.count, _$this0.doubleCount); + } + }; + return {count}{doubleCount}; + }); + return
{doubleCount2}
; + }); `); - expect(true).toHaveLength(1); + const div = root.children![0] as any; + expect(div.children[0].content.dependencyIndexArr).toMatchInlineSnapshot(` + [ + 4, + 3, + 2, + 0, + ] + `); + expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount2]"'); + expect(div.props.className.dependencyIndexArr).toMatchInlineSnapshot(` + [ + 1, + 2, + 0, + ] + `); + expect(genCode(div.props.className.value)).toMatchInlineSnapshot('"className + count"'); + + // @ts-expect-error ignore ts here + const InputCompNode = (root.variables[3] as ComponentNode).value; + expect(InputCompNode.usedPropertySet).toMatchInlineSnapshot(` + Set { + "count", + "doubleCount", + "name", + } + `); + // it's the {count} + const inputFirstExp = InputCompNode.children[0].children[0]; + expect(inputFirstExp.content.dependencyIndexArr).toMatchInlineSnapshot(` + [ + 5, + ] + `); + expect(genCode(inputFirstExp.content.dependenciesNode)).toMatchInlineSnapshot('"[count]"'); + + // it's the {doubleCount} + const inputSecondExp = InputCompNode.children[0].children[1]; + expect(inputSecondExp.content.dependencyIndexArr).toMatchInlineSnapshot(` + [ + 3, + 2, + 0, + ] + `); + expect(genCode(inputSecondExp.content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount]"'); }); }); 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 36f436b6..2140aa03 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,4 +1,4 @@ -import { functionalMacroAnalyze } from '../../src/analyze/functionalMacroAnalyze'; +import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze'; import { genCode, mockAnalyze } from '../mock'; import { describe, expect, it } from 'vitest'; diff --git a/packages/transpiler/babel-inula-next-core/test/condition.test.x.tsx b/packages/transpiler/babel-inula-next-core/test/e2e/condition.test.x.tsx similarity index 98% rename from packages/transpiler/babel-inula-next-core/test/condition.test.x.tsx rename to packages/transpiler/babel-inula-next-core/test/e2e/condition.test.x.tsx index 4e95cf02..2902ff3c 100644 --- a/packages/transpiler/babel-inula-next-core/test/condition.test.x.tsx +++ b/packages/transpiler/babel-inula-next-core/test/e2e/condition.test.x.tsx @@ -14,7 +14,7 @@ */ import { describe, expect, it } from 'vitest'; -import { transform } from './presets'; +import { transform } from '../presets'; describe('condition', () => { it('should transform jsx', () => { diff --git a/packages/transpiler/babel-inula-next-core/test/mock.ts b/packages/transpiler/babel-inula-next-core/test/mock.ts index 1ac853c2..5a0c09bd 100644 --- a/packages/transpiler/babel-inula-next-core/test/mock.ts +++ b/packages/transpiler/babel-inula-next-core/test/mock.ts @@ -13,13 +13,13 @@ * See the Mulan PSL v2 for more details. */ -import { Analyzer, ComponentNode, InulaNode } from '../src/analyze/types'; +import { Analyzer, ComponentNode } from '../src/analyzer/types'; import { type PluginObj, transform as transformWithBabel } from '@babel/core'; import syntaxJSX from '@babel/plugin-syntax-jsx'; -import { analyze } from '../src/analyze'; +import { analyze } from '../src/analyzer'; import generate from '@babel/generator'; import * as t from '@babel/types'; -import { register } from '../src/babelTypes'; +import { register } from '@openinula/babel-api'; import { defaultHTMLTags } from '../src/const'; export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode { @@ -63,26 +63,3 @@ export function genCode(ast: t.Node | null) { } return generate(ast).code; } - -export function printTree(node: InulaNode | undefined): any { - if (!node) { - return 'empty'; - } - if (node.type === 'cond') { - return { - type: node.type, - branch: node.branches.map(b => printTree(b.content)), - children: printTree(node.child), - }; - } else if (node.type === 'comp') { - return { - type: node.type, - children: printTree(node.child), - }; - } else if (node.type === 'jsx') { - return { - type: node.type, - }; - } - return 'unknown'; -} diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/earlyReturnAnanlyze.test.x.ts b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/earlyReturnAnanlyze.test.x.ts index fe75bc19..7ebfb786 100644 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/earlyReturnAnanlyze.test.x.ts +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/earlyReturnAnanlyze.test.x.ts @@ -14,7 +14,7 @@ */ import { describe, expect, it } from 'vitest'; -import { isCondNode } from '../../src/analyze'; +import { isCondNode } from '../../src/analyzer'; import { mockAnalyze } from '../mock'; describe('analyze early return', () => { @@ -30,7 +30,7 @@ describe('analyze early return', () => { ; } `); - const branchNode = root?.child; + const branchNode = root?.children; if (!isCondNode(branchNode)) { throw new Error('Should be branch node'); } @@ -49,7 +49,7 @@ describe('analyze early return', () => { return
; } `); - const branchNode = root?.child; + const branchNode = root?.children; if (!isCondNode(branchNode)) { throw new Error('Should be branch node'); } @@ -73,7 +73,7 @@ describe('analyze early return', () => { return
; } `); - const branchNode = root?.child; + const branchNode = root?.children; if (!isCondNode(branchNode)) { throw new Error('Should be branch node'); } diff --git a/packages/transpiler/babel-preset-inula-next/package.json b/packages/transpiler/babel-preset-inula-next/package.json index a7b33eec..1bd04635 100644 --- a/packages/transpiler/babel-preset-inula-next/package.json +++ b/packages/transpiler/babel-preset-inula-next/package.json @@ -44,7 +44,7 @@ "@types/babel__generator": "^7.6.8", "@types/babel__parser": "^7.1.1", "@types/babel__traverse": "^7.6.8", - "jsx-view-parser": "workspace:*", + "@openinula/jsx-view-parser": "workspace:*", "minimatch": "^9.0.3", "vitest": "^1.4.0" }, diff --git a/packages/transpiler/babel-preset-inula-next/src/main.ts b/packages/transpiler/babel-preset-inula-next/src/main.ts index 23a43d13..aba8dd59 100644 --- a/packages/transpiler/babel-preset-inula-next/src/main.ts +++ b/packages/transpiler/babel-preset-inula-next/src/main.ts @@ -3,7 +3,7 @@ import { type PluginObj } from '@babel/core'; import { PluginProviderClass } from './pluginProvider'; import { type DLightOption } from './types'; import { defaultAttributeMap } from './const'; -import { analyze } from './analyze'; +import { analyze } from './analyzer'; export default function (api: typeof babel, options: DLightOption): PluginObj { const { types } = api; diff --git a/packages/transpiler/babel-preset-inula-next/src/pluginProvider.ts b/packages/transpiler/babel-preset-inula-next/src/pluginProvider.ts index 386fec16..1a5af5d9 100644 --- a/packages/transpiler/babel-preset-inula-next/src/pluginProvider.ts +++ b/packages/transpiler/babel-preset-inula-next/src/pluginProvider.ts @@ -3,7 +3,7 @@ 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 'jsx-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 { diff --git a/packages/transpiler/babel-preset-inula-next/test/earlyReturnAnanlyze.test.ts b/packages/transpiler/babel-preset-inula-next/test/earlyReturnAnanlyze.test.ts index 51284d14..aefca3df 100644 --- a/packages/transpiler/babel-preset-inula-next/test/earlyReturnAnanlyze.test.ts +++ b/packages/transpiler/babel-preset-inula-next/test/earlyReturnAnanlyze.test.ts @@ -14,7 +14,7 @@ */ import { describe, expect, it } from 'vitest'; -import { isCondNode } from '../src/analyze'; +import { isCondNode } from '../src/analyzer'; import { mockAnalyze } from './mock'; describe('analyze early return', () => { diff --git a/packages/transpiler/babel-preset-inula-next/test/mock.ts b/packages/transpiler/babel-preset-inula-next/test/mock.ts index 6c447d66..5d16a76b 100644 --- a/packages/transpiler/babel-preset-inula-next/test/mock.ts +++ b/packages/transpiler/babel-preset-inula-next/test/mock.ts @@ -16,7 +16,7 @@ import { ComponentNode, InulaNode } from '../src/analyze/types'; import babel, { type PluginObj, transform as transformWithBabel } from '@babel/core'; import syntaxJSX from '@babel/plugin-syntax-jsx'; -import { analyze } from '../src/analyze'; +import { analyze } from '../src/analyzer'; import generate from '@babel/generator'; import * as t from '@babel/types'; diff --git a/packages/transpiler/jsx-parser/package.json b/packages/transpiler/jsx-parser/package.json index b9c8f688..bbbfad9f 100644 --- a/packages/transpiler/jsx-parser/package.json +++ b/packages/transpiler/jsx-parser/package.json @@ -1,5 +1,5 @@ { - "name": "jsx-view-parser", + "name": "@openinula/jsx-view-parser", "version": "0.0.1", "description": "Inula jsx parser", "author": { diff --git a/packages/transpiler/reactivity-parser/package.json b/packages/transpiler/reactivity-parser/package.json index 023a7da4..5d42566a 100644 --- a/packages/transpiler/reactivity-parser/package.json +++ b/packages/transpiler/reactivity-parser/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@openinula/error-handler": "workspace:*", + "@openinula/jsx-view-parser": "workspace:*", "@openinula/view-parser": "workspace:*" }, "tsup": { diff --git a/packages/transpiler/reactivity-parser/src/index.ts b/packages/transpiler/reactivity-parser/src/index.ts index 06076d17..f3a27edb 100644 --- a/packages/transpiler/reactivity-parser/src/index.ts +++ b/packages/transpiler/reactivity-parser/src/index.ts @@ -1,4 +1,4 @@ -import { type ViewUnit } from '@openinula/view-parser'; +import { type ViewUnit } from '@openinula/jsx-view-parser'; import { ReactivityParser } from './parser'; import { type ViewParticle, type ReactivityParserConfig } from './types'; @@ -6,7 +6,6 @@ import { type ViewParticle, type ReactivityParserConfig } from './types'; * @brief Parse view units to get used properties and view particles with reactivity * @param viewUnits * @param config - * @param options * @returns [viewParticles, usedProperties] */ export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserConfig): [ViewParticle[], Set] { @@ -21,5 +20,9 @@ export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserC }); return [dlParticles, usedProperties]; } +/** + * The key to get the previous map in DependencyMap Chain + */ +export const PrevMap = Symbol('prevMap'); export type * from './types'; diff --git a/packages/transpiler/reactivity-parser/src/parser.ts b/packages/transpiler/reactivity-parser/src/parser.ts index ce708123..23334244 100644 --- a/packages/transpiler/reactivity-parser/src/parser.ts +++ b/packages/transpiler/reactivity-parser/src/parser.ts @@ -12,9 +12,7 @@ import { type ForParticle, type IfParticle, type EnvParticle, - type SnippetParticle, - SwitchParticle, - TryParticle, + DependencyMap, } from './types'; import { type NodePath, type types as t, type traverse } from '@babel/core'; import { @@ -22,16 +20,14 @@ import { type HTMLUnit, type ViewUnit, type CompUnit, - type ViewProp, + type UnitProp, type ForUnit, type IfUnit, type EnvUnit, type ExpUnit, - type SnippetUnit, - SwitchUnit, - TryUnit, -} from '@openinula/view-parser'; +} from '@openinula/jsx-view-parser'; import { DLError } from './error'; +import { PrevMap } from '.'; export class ReactivityParser { private readonly config: ReactivityParserConfig; @@ -40,7 +36,7 @@ export class ReactivityParser { private readonly traverse: typeof traverse; private readonly availableProperties: string[]; private readonly availableIdentifiers?: string[]; - private readonly dependencyMap: Record; + private readonly dependencyMap: DependencyMap; private readonly identifierDepMap: Record; private readonly dependencyParseType; private readonly reactivityFuncNames; @@ -99,12 +95,9 @@ export class ReactivityParser { if (viewUnit.type === 'html') return this.parseHTML(viewUnit); if (viewUnit.type === 'comp') return this.parseComp(viewUnit); if (viewUnit.type === 'for') return this.parseFor(viewUnit); - if (viewUnit.type === 'try') return this.parseTry(viewUnit); if (viewUnit.type === 'if') return this.parseIf(viewUnit); if (viewUnit.type === 'env') return this.parseEnv(viewUnit); if (viewUnit.type === 'exp') return this.parseExp(viewUnit); - if (viewUnit.type === 'switch') return this.parseSwitch(viewUnit); - if (viewUnit.type === 'snippet') return this.parseSnippet(viewUnit); return DLError.throw1(); } @@ -420,45 +413,6 @@ export class ReactivityParser { }; } - // ---- @Switch ---- - /** - * @brief Parse a SwitchUnit into an SwitchParticle with dependencies - * @param switchUnit - * @returns SwitchParticle - */ - private parseSwitch(switchUnit: SwitchUnit): SwitchParticle { - return { - type: 'switch', - discriminant: { - value: switchUnit.discriminant, - ...this.getDependencies(switchUnit.discriminant), - }, - branches: switchUnit.branches.map(branch => ({ - case: { - value: branch.case, - ...this.getDependencies(branch.case), - }, - children: branch.children.map(this.parseViewParticle.bind(this)), - break: branch.break, - })), - }; - } - - // ---- @Try ---- - /** - * @brief Parse a TryUnit into an TryParticle with dependencies - * @param tryUnit - * @returns TryParticle - */ - private parseTry(tryUnit: TryUnit): TryParticle { - return { - type: 'try', - children: tryUnit.children.map(this.parseViewParticle.bind(this)), - exception: tryUnit.exception, - catchChildren: tryUnit.catchChildren.map(this.parseViewParticle.bind(this)), - }; - } - // ---- @Env ---- /** * @brief Parse an EnvUnit into an EnvParticle with dependencies @@ -492,38 +446,13 @@ export class ReactivityParser { return expParticle; } - // ---- @Snippet ---- - /** - * @brief Parse a SnippetUnit into a SnippetParticle with dependencies - * @param snippetUnit - * @returns SnippetParticle - */ - private parseSnippet(snippetUnit: SnippetUnit): SnippetParticle { - const snippetParticle: SnippetParticle = { - type: 'snippet', - tag: snippetUnit.tag, - props: {}, - children: [], - }; - if (snippetUnit.props) { - snippetParticle.props = Object.fromEntries( - Object.entries(snippetUnit.props).map(([key, prop]) => [key, this.generateDependencyProp(prop)]) - ); - } - if (snippetUnit.children) { - snippetParticle.children = snippetUnit.children.map(this.parseViewParticle.bind(this)); - } - - return snippetParticle; - } - // ---- Dependencies ---- /** * @brief Generate a dependency prop with dependencies * @param prop * @returns DependencyProp */ - private generateDependencyProp(prop: ViewProp): DependencyProp { + private generateDependencyProp(prop: UnitProp): DependencyProp { const dependencyProp: DependencyProp = { value: prop.value, ...this.getDependencies(prop.value), @@ -559,13 +488,12 @@ export class ReactivityParser { // ---- Both id and prop deps need to be calculated because // id is for snippet update, prop is normal update // in a snippet, the depsNode should be both id and prop - const [directIdentifierDeps, identifierDepNodes] = this.getIdentifierDependencies(node); const [directPropertyDeps, propertyDepNodes] = this.getPropertyDependencies(node); - const directDependencies = this.dependencyParseType === 'identifier' ? directIdentifierDeps : directPropertyDeps; + const directDependencies = directPropertyDeps; const identifierMapDependencies = this.getIdentifierMapDependencies(node); const deps = [...new Set([...directDependencies, ...identifierMapDependencies])]; - const depNodes = [...identifierDepNodes, ...propertyDepNodes] as t.Expression[]; + const depNodes = [...propertyDepNodes] as t.Expression[]; return { dynamic: depNodes.length > 0 || deps.length > 0, @@ -625,7 +553,7 @@ export class ReactivityParser { }); deps.forEach(this.usedProperties.add.bind(this.usedProperties)); - return [[...deps].map(dep => this.availableProperties.indexOf(dep)), dependencyNodes]; + return [[...deps].map(dep => this.availableProperties.lastIndexOf(dep)), dependencyNodes]; } /** @@ -646,9 +574,9 @@ export class ReactivityParser { const wrappedNode = this.valueWrapper(node); this.traverse(wrappedNode, { - MemberExpression: innerPath => { - if (!this.t.isIdentifier(innerPath.node.property) || !this.t.isThisExpression(innerPath.node.object)) return; - const propertyKey = innerPath.node.property.name; + Identifier: innerPath => { + const propertyKey = innerPath.node.name; + if (this.isAssignmentExpressionLeft(innerPath) || this.isAssignmentFunction(innerPath)) { assignDeps.add(propertyKey); } else if ( @@ -657,13 +585,12 @@ export class ReactivityParser { !this.isMemberInManualFunction(innerPath) ) { deps.add(propertyKey); - this.dependencyMap[propertyKey]?.forEach(deps.add.bind(deps)); if (!depNodes[propertyKey]) depNodes[propertyKey] = []; depNodes[propertyKey].push(this.geneDependencyNode(innerPath)); } }, }); - + const dependencyIdxArr = deduplicate([...deps].map(this.calDependencyIndexArr).flat()); assignDeps.forEach(dep => { deps.delete(dep); delete depNodes[dep]; @@ -676,7 +603,36 @@ export class ReactivityParser { }); deps.forEach(this.usedProperties.add.bind(this.usedProperties)); - return [[...deps].map(dep => this.availableProperties.indexOf(dep)), dependencyNodes]; + return [dependencyIdxArr, dependencyNodes]; + } + + private calDependencyIndexArr = (directDepKey: string) => { + // iterate the availableProperties reversely to find the index of the property + // cause the availableProperties is in the order of the code + const chainedDepKeys = this.findDependency(directDepKey); + const depKeyQueue = chainedDepKeys ? [directDepKey, ...chainedDepKeys] : [directDepKey]; + depKeyQueue.forEach(this.usedProperties.add.bind(this.usedProperties)); + + let dep = depKeyQueue.shift(); + const result: number[] = []; + for (let i = this.availableProperties.length - 1; i >= 0; i--) { + if (this.availableProperties[i] === dep) { + result.push(i); + dep = depKeyQueue.shift(); + } + } + return result; + }; + + private findDependency(propertyKey: string) { + let currentMap: DependencyMap | undefined = this.dependencyMap; + do { + if (currentMap[propertyKey] !== undefined) { + return currentMap[propertyKey]; + } + currentMap = currentMap[PrevMap]; + } while (currentMap); + return null; } /** @@ -738,7 +694,7 @@ export class ReactivityParser { }); deps.forEach(this.usedProperties.add.bind(this.usedProperties)); - return [...deps].map(dep => this.availableProperties.indexOf(dep)); + return [...deps].map(dep => this.availableProperties.lastIndexOf(dep)); } // ---- Utils ---- @@ -781,7 +737,7 @@ export class ReactivityParser { * @param prop * @returns is a static prop */ - private isStaticProp(prop: ViewProp): boolean { + private isStaticProp(prop: UnitProp): boolean { const { value, viewPropMap } = prop; return ( (!viewPropMap || Object.keys(viewPropMap).length === 0) && @@ -950,10 +906,9 @@ export class ReactivityParser { } if (!parentPath) return false; return ( - this.t.isCallExpression(parentPath.node) && - this.t.isMemberExpression(parentPath.node.callee) && - this.t.isIdentifier(parentPath.node.callee.property) && - this.reactivityFuncNames.includes(parentPath.node.callee.property.name) + parentPath.isCallExpression() && + parentPath.get('callee').isIdentifier() && + this.reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name) ); } @@ -1021,3 +976,7 @@ export class ReactivityParser { return Math.random().toString(36).slice(2); } } + +function deduplicate(arr: T[]): T[] { + return [...new Set(arr)]; +} diff --git a/packages/transpiler/reactivity-parser/src/test/Dependency.test.ts b/packages/transpiler/reactivity-parser/src/test/Dependency.test.ts index d7b86ca6..217ec5e5 100644 --- a/packages/transpiler/reactivity-parser/src/test/Dependency.test.ts +++ b/packages/transpiler/reactivity-parser/src/test/Dependency.test.ts @@ -4,13 +4,13 @@ import { type CompParticle } from '../types'; describe('Dependency', () => { it('should parse the correct dependency', () => { - const viewParticles = parse('Comp(this.flag)'); + const viewParticles = parse('Comp(flag)'); const content = (viewParticles[0] as CompParticle).props._$content; expect(content?.dependencyIndexArr).toContain(0); }); it('should parse the correct dependency when interfacing the dependency chain', () => { - const viewParticles = parse('Comp(this.doubleCount)'); + const viewParticles = parse('Comp(doubleCount)'); const content = (viewParticles[0] as CompParticle).props._$content; const dependency = content?.dependencyIndexArr; // ---- doubleCount depends on count, count depends on flag @@ -21,47 +21,41 @@ describe('Dependency', () => { }); it('should not parse the dependency if the property is not in the availableProperties', () => { - const viewParticles = parse('Comp(this.notExist)'); - const content = (viewParticles[0] as CompParticle).props._$content; - expect(content?.dependencyIndexArr).toHaveLength(0); - }); - - it('should not parse the dependency if the identifier is not an property of a ThisExpression', () => { - const viewParticles = parse('Comp(count)'); + const viewParticles = parse('Comp(notExist)'); const content = (viewParticles[0] as CompParticle).props._$content; expect(content?.dependencyIndexArr).toHaveLength(0); }); it('should not parse the dependency if the member expression is in an escaped function', () => { - let viewParticles = parse('Comp(escape(this.flag))'); + let viewParticles = parse('Comp(escape(flag))'); let content = (viewParticles[0] as CompParticle).props._$content; expect(content?.dependencyIndexArr).toHaveLength(0); - viewParticles = parse('Comp($(this.flag))'); + viewParticles = parse('Comp($(flag))'); content = (viewParticles[0] as CompParticle).props._$content; expect(content?.dependencyIndexArr).toHaveLength(0); }); it('should not parse the dependency if the member expression is in a manual function', () => { - const viewParticles = parse('Comp(manual(() => this.count, []))'); + const viewParticles = parse('Comp(manual(() => count, []))'); const content = (viewParticles[0] as CompParticle).props._$content; expect(content?.dependencyIndexArr).toHaveLength(0); }); it("should parse the dependencies in manual function's second parameter", () => { - const viewParticles = parse('Comp(manual(() => {let a = this.count}, [this.flag]))'); + const viewParticles = parse('Comp(manual(() => {let a = count}, [flag]))'); const content = (viewParticles[0] as CompParticle).props._$content; expect(content?.dependencyIndexArr).toHaveLength(1); }); it('should not parse the dependency if the member expression is the left side of an assignment expression', () => { - const viewParticles = parse('Comp(this.flag = 1)'); + const viewParticles = parse('Comp(flag = 1)'); const content = (viewParticles[0] as CompParticle).props._$content; expect(content?.dependencyIndexArr).toHaveLength(0); }); it('should not parse the dependency if the member expression is right side of an assignment expression', () => { - const viewParticles = parse('Comp(this.flag = this.flag + 1)'); + const viewParticles = parse('Comp(flag = flag + 1)'); const content = (viewParticles[0] as CompParticle).props._$content; expect(content?.dependencyIndexArr).toHaveLength(0); }); diff --git a/packages/transpiler/reactivity-parser/src/test/MutableTagParticle.test.ts b/packages/transpiler/reactivity-parser/src/test/MutableTagParticle.test.ts index 4037b8a7..5f02bd80 100644 --- a/packages/transpiler/reactivity-parser/src/test/MutableTagParticle.test.ts +++ b/packages/transpiler/reactivity-parser/src/test/MutableTagParticle.test.ts @@ -6,7 +6,7 @@ import { type types as t } from '@babel/core'; describe('MutableTagParticle', () => { // ---- HTML it('should parse an HTMLUnit with dynamic tag as an HTMLParticle', () => { - const viewParticles = parse('tag(this.div)()'); + const viewParticles = parse('tag(div)()'); expect(viewParticles.length).toBe(1); expect(viewParticles[0].type).toBe('html'); }); @@ -18,7 +18,7 @@ describe('MutableTagParticle', () => { }); it('should parse an HTMLUnit with non-static-html children as an HTMLParticle', () => { - const viewParticles = parse('div(); { Comp(); tag(this.div)(); }'); + const viewParticles = parse('div(); { Comp(); tag(div)(); }'); expect(viewParticles.length).toBe(1); expect(viewParticles[0].type).toBe('html'); }); @@ -30,7 +30,7 @@ describe('MutableTagParticle', () => { }); it('should parse an HTMLUnit with dynamic tag with dependencies as an ExpParticle', () => { - const viewParticles = parse('tag(this.flag)()'); + const viewParticles = parse('tag(flag)()'); expect(viewParticles.length).toBe(1); expect(viewParticles[0].type).toBe('exp'); const content = (viewParticles[0] as ExpParticle).content; @@ -48,7 +48,7 @@ describe('MutableTagParticle', () => { }); it('should parse a CompUnit with dynamic tag with dependencies as an ExpParticle', () => { - const viewParticles = parse('comp(CompList[this.flag])()'); + const viewParticles = parse('comp(CompList[flag])()'); expect(viewParticles.length).toBe(1); expect(viewParticles[0].type).toBe('exp'); const content = (viewParticles[0] as ExpParticle).content; @@ -61,7 +61,7 @@ describe('MutableTagParticle', () => { // ---- Snippet it('should parse a SnippetUnit as an HTMLParticle', () => { - const viewParticles = parse('this.MySnippet()'); + const viewParticles = parse('MySnippet()'); expect(viewParticles.length).toBe(1); expect(viewParticles[0].type).toBe('snippet'); }); diff --git a/packages/transpiler/reactivity-parser/src/types.ts b/packages/transpiler/reactivity-parser/src/types.ts index 590e017d..7ac6dfb5 100644 --- a/packages/transpiler/reactivity-parser/src/types.ts +++ b/packages/transpiler/reactivity-parser/src/types.ts @@ -1,10 +1,11 @@ import { type types as t } from '@babel/core'; import type Babel from '@babel/core'; +import { PrevMap } from '.'; export interface DependencyValue { value: T; - dynamic: boolean; - dependencyIndexArr: number[]; + dynamic: boolean; // to removed + dependencyIndexArr: number[]; // -> bit dependenciesNode: t.ArrayExpression; } @@ -127,9 +128,25 @@ export interface ReactivityParserConfig { babelApi: typeof Babel; availableProperties: string[]; availableIdentifiers?: string[]; - dependencyMap: Record; + dependencyMap: Record; identifierDepMap?: Record; dependencyParseType?: 'property' | 'identifier'; parseTemplate?: boolean; reactivityFuncNames?: string[]; } + +export interface DependencyMap { + /** + * key is the variable name, value is the dependencies + * i.e. { + * count: ['flag'], + * state1: ['count', 'flag'], + * state2: ['count', 'flag', 'state1'], + * state3: ['count', 'flag', 'state1', 'state2'], + * state4: ['count', 'flag', 'state1', 'state2', 'state3'], + * } + */ + [key: string]: string[] | null; + + [PrevMap]?: DependencyMap; +}