diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/index.ts b/packages/transpiler/babel-inula-next-core/src/analyze/index.ts index c5d28dbd..b7da4b34 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/index.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/index.ts @@ -1,13 +1,12 @@ -import { NodePath } from '@babel/core'; -import { jsxSlicesAnalyze } from './jsxSliceAnalyze'; -import { earlyReturnAnalyze } from './earlyReturnAnalyze'; +import { type types as t, type NodePath } from '@babel/core'; +import { propsAnalyze } from './propsAnalyze'; import { AnalyzeContext, Analyzer, ComponentNode, CondNode, Visitor } from './types'; import { createComponentNode } from './nodeFactory'; import { propertiesAnalyze } from './propertiesAnalyze'; -import { isValidComponent } from './utils'; -import * as t from '@babel/types'; +import { lifeCycleAnalyze } from './lifeCycleAnalyze'; import { getFnBody } from '../utils'; -const builtinAnalyzers = [jsxSlicesAnalyze, earlyReturnAnalyze, propertiesAnalyze]; + +const builtinAnalyzers = [propsAnalyze, propertiesAnalyze, lifeCycleAnalyze]; let analyzers: Analyzer[] = builtinAnalyzers; export function isCondNode(node: any): node is CondNode { @@ -15,53 +14,73 @@ export function isCondNode(node: any): node is CondNode { } function mergeVisitor(...visitors: Analyzer[]): Visitor { - return visitors.reduce((acc, cur) => { - return { - ...acc, - ...cur(), - }; + return visitors.reduce>((acc, cur) => { + const visitor = cur(); + const visitorKeys = Object.keys(visitor) as (keyof Visitor)[]; + for (const key of visitorKeys) { + if (acc[key]) { + // if already exist, merge the visitor function + const original = acc[key]!; + acc[key] = (path: any, ctx) => { + original(path, ctx); + visitor[key]?.(path, ctx); + }; + } else { + // @ts-expect-error key is a valid key, no idea why it's not working + acc[key] = visitor[key]; + } + } + return acc; }, {}); } // walk through the function component body -export function iterateFCBody(bodyStatements: NodePath[], componentNode: ComponentNode, level = 0) { +export function analyzeFnComp( + types: typeof t, + fnNode: NodePath, + componentNode: ComponentNode, + level = 0 +) { const visitor = mergeVisitor(...analyzers); - const visit = (p: NodePath, ctx: AnalyzeContext) => { + const context: AnalyzeContext = { + level, + t: types, + current: componentNode, + traverse: (path: NodePath, ctx: AnalyzeContext) => { + path.traverse(visitor, ctx); + }, + }; + // --- analyze the function props --- + const params = fnNode.get('params'); + const props = params[0]; + if (props) { + if (props.isObjectPattern()) { + props.get('properties').forEach(prop => { + visitor.Prop?.(prop, context); + }); + } else { + throw new Error( + `Component ${componentNode.name}: The first parameter of the function component must be an object pattern` + ); + } + } + + // --- analyze the function body --- + const bodyStatements = getFnBody(fnNode).get('body'); + for (let i = 0; i < bodyStatements.length; i++) { + const p = bodyStatements[i]; + const type = p.node.type; // TODO: More type safe way to handle this - visitor[type]?.(p as unknown as any, ctx); - }; - for (let i = 0; i < bodyStatements.length; i++) { - const p = bodyStatements[i]; - let skipRest = false; - const context: AnalyzeContext = { - level, - index: i, - currentComponent: componentNode, - restStmt: bodyStatements.slice(i + 1), - skipRest() { - skipRest = true; - }, - traverse: (path: NodePath, ctx: AnalyzeContext) => { - // @ts-expect-error TODO: fix visitor type incompatibility - path.traverse(visitor, ctx); - }, - }; - - visit(p, context); + visitor[type]?.(p as unknown as any, context); if (p.isReturnStatement()) { visitor.ReturnStatement?.(p, context); break; } - - if (skipRest) { - break; - } } } - /** * The process of analyzing the component * 1. identify the component @@ -69,10 +88,13 @@ export function iterateFCBody(bodyStatements: NodePath[], component * 2. identify the component's props, including children, alias, and default value * 3. analyze the early return of the component, build into the branch * + * @param types + * @param fnName * @param path * @param customAnalyzers */ export function analyze( + types: typeof t, fnName: string, path: NodePath, customAnalyzers?: Analyzer[] @@ -81,7 +103,8 @@ export function analyze( analyzers = customAnalyzers; } - const root = createComponentNode(fnName, getFnBody(path)); + const root = createComponentNode(fnName, path); + analyzeFnComp(types, path, root); return root; } diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/lifeCycleAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/lifeCycleAnalyze.ts index 9dcf2000..8151e847 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/lifeCycleAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/lifeCycleAnalyze.ts @@ -32,13 +32,16 @@ function isLifeCycleName(name: string): name is LifeCycle { */ export function lifeCycleAnalyze(): Visitor { return { - CallExpression(path: NodePath, ctx) { - const callee = path.get('callee'); - if (callee.isIdentifier(path)) { - const lifeCycleName = callee.node.name; - if (isLifeCycleName(lifeCycleName)) { - const fnNode = extractFnFromMacro(path, lifeCycleName); - addLifecycle(ctx.currentComponent, lifeCycleName, getFnBody(fnNode)); + ExpressionStatement(path: NodePath, ctx) { + const expression = path.get('expression'); + if (expression.isCallExpression()) { + const callee = expression.get('callee'); + if (callee.isIdentifier()) { + const lifeCycleName = callee.node.name; + if (isLifeCycleName(lifeCycleName)) { + const fnNode = extractFnFromMacro(expression, lifeCycleName); + addLifecycle(ctx.current, lifeCycleName, getFnBody(fnNode)); + } } } }, diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts b/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts index 51f2fc02..bdbfe6a2 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts @@ -15,44 +15,87 @@ import { NodePath, type types as t } from '@babel/core'; import { Branch, ComponentNode, CondNode, InulaNode, JSX, JSXNode, LifeCycle, SubCompNode } from './types'; -import { iterateFCBody } from './index'; +import { PropType } from '../constants'; export function createComponentNode( name: string, - fnBody: NodePath[], + fnNode: NodePath, parent?: ComponentNode ): ComponentNode { const comp: ComponentNode = { type: 'comp', name, - props: {}, + props: [], child: undefined, subComponents: [], - methods: [], - state: [], + properties: [], + dependencyMap: {}, + reactiveMap: {}, + lifecycle: {}, parent, - fnBody, + // fnBody, + get availableProperties() { + return comp.properties + .filter(({ isMethod }) => !isMethod) + .map(({ name }) => name) + .concat( + comp.props + .map(({ name, nestedProps, alias }) => { + const nested = nestedProps ? nestedProps.map(name => name) : []; + return [alias ? alias : name, ...nested]; + }) + .flat() + ); + }, }; - iterateFCBody(fnBody, comp); - return comp; } -export function addState(comp: ComponentNode, name: string, value: t.Expression | null) { - comp.state.push({ name, value }); +export function addProperty( + comp: ComponentNode, + name: string, + value: t.Expression | null, + isComputed: boolean, + isMethod = false +) { + comp.properties.push({ name, value, isComputed, isMethod }); } -export function addMethod(comp: ComponentNode, method: NodePath) { - comp.methods.push(method); +export function addMethod(comp: ComponentNode, name: string, value: t.Expression | null) { + comp.properties.push({ name, value, isComputed: false, isMethod: true }); } -export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, stmts: NodePath[]) { +export function addProp( + comp: ComponentNode, + type: PropType, + key: string, + defaultVal: t.Expression | null = null, + alias: string | null = null, + nestedProps: string[] | null = null, + nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null +) { + comp.props.push({ name: key, type, default: defaultVal, alias, nestedProps, nestedRelationship }); +} + +export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: NodePath) { const compLifecycle = comp.lifecycle; if (!compLifecycle[lifeCycle]) { compLifecycle[lifeCycle] = []; } - compLifecycle[lifeCycle].push(stmts); + compLifecycle[lifeCycle]!.push(block); +} + +export function addWatch( + comp: ComponentNode, + callback: NodePath | NodePath, + deps: NodePath | null +) { + // if watch not exist, create a new one + if (!comp.watch) { + comp.watch = []; + } + comp.watch.push({ callback, deps }); } export function createJSXNode(parent: ComponentNode, content: NodePath): JSXNode { diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/propertiesAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/propertiesAnalyze.ts index d585983c..d71a90e3 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/propertiesAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/propertiesAnalyze.ts @@ -13,40 +13,13 @@ * See the Mulan PSL v2 for more details. */ -import { NodePath } from '@babel/core'; +import { AnalyzeContext, Visitor } from './types'; +import { addLifecycle, addMethod, addProperty } 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 { Visitor } from './types'; -import { addMethod, addState } from './nodeFactory'; -import { hasJSX, isValidComponentName, isValidPath } from './utils'; -import { jsxSlicesAnalyze } from './jsxSliceAnalyze'; -import * as t from '@babel/types'; - -// Analyze the JSX slice in the function component, including: -// 1. VariableDeclaration, like `const a =
` -// 2. SubComponent, like `function Sub() { return
}` -function handleFn(fnName: string, fnBody: NodePath) { - if (isValidComponentName(fnName)) { - // This is a subcomponent, treat it as a normal component - } else { - // This is jsx creation function - // function jsxFunc() { - // // This is a function that returns JSX - // // because the function name is smallCamelCased - // return
{count}
- // } - // => - // function jsxFunc() { - // function Comp_$id4$() { - // return
{count}
- // } - // // This is a function that returns JSX - // // because the function name is smallCamelCased - // return - // } - } -} - -// 3. jsx creation function, like `function create() { return
}` export function propertiesAnalyze(): Visitor { return { VariableDeclaration(path: NodePath, ctx) { @@ -61,43 +34,121 @@ 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--- const init = declaration.get('init'); - if (isValidPath(init) && hasJSX(init)) { - if (init.isArrowFunctionExpression()) { - const fnName = id.node.name; - const fnBody = init.get('body'); - - // handle case like `const jsxFunc = () =>
` - if (fnBody.isExpression()) { - // turn expression into block statement for consistency - fnBody.replaceWith(t.blockStatement([t.returnStatement(fnBody.node)])); - } - - // We switched to the block statement above, so we can safely call handleFn - handleFn(fnName, fnBody as NodePath); + let deps: string[] | null = null; + if (isValidPath(init)) { + if (init.isArrowFunctionExpression() || init.isFunctionExpression()) { + addMethod(ctx.current, id.node.name, init.node); + return; } - // handle jsx slice - ctx.traverse(path, ctx); + deps = getDependenciesFromNode(id.node.name, init, ctx); } - addState(ctx.currentComponent, id.node.name, declaration.node.init || null); + addProperty(ctx.current, id.node.name, init.node || null, !!deps?.length); } }); }, - FunctionDeclaration(path: NodePath, ctx) { + FunctionDeclaration(path: NodePath, { current }) { const fnId = path.node.id; if (!fnId) { - // This is an anonymous function, collect into lifecycle - //TODO - return; + throw new Error('Function declaration must have an id'); } - if (!hasJSX(path)) { - // This is a normal function, collect into methods - addMethod(ctx.currentComponent, path); - return; - } - - handleFn(fnId.name, path.get('body')); + const functionExpression = types.functionExpression( + path.node.id, + path.node.params, + path.node.body, + path.node.generator, + path.node.async + ); + addMethod(current, fnId.name, functionExpression); }, }; } + +/** + * @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.availableProperties.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/propsAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/propsAnalyze.ts index e69de29b..654dd95f 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/propsAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/propsAnalyze.ts @@ -0,0 +1,106 @@ +import { type types as t, type NodePath } from '@babel/core'; +import { AnalyzeContext, Visitor } from './types'; +import { addProp } from './nodeFactory'; +import { PropType } from '../constants'; +import { types } from '../babelTypes'; +function analyzeSingleProp( + value: t.ObjectProperty['value'], + key: string, + path: NodePath, + { t, current }: AnalyzeContext +) { + let defaultVal: t.Expression | null = null; + let alias: string | null = null; + const nestedProps: string[] | null = []; + let nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null; + if (t.isIdentifier(value)) { + // 1. handle alias without default value + // handle alias without default value + if (key !== value.name) { + alias = value.name; + } + } else if (t.isAssignmentPattern(value)) { + // 2. handle default value case + const assignedName = value.left; + defaultVal = value.right; + if (t.isIdentifier(assignedName)) { + if (assignedName.name !== key) { + // handle alias in default value case + alias = assignedName.name; + } + } else { + throw Error(`Unsupported assignment type in object destructuring: ${assignedName.type}`); + } + } 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}]} + // we should collect prop1, p20X, p211, p212X + path.get('value').traverse({ + Identifier(path) { + // judge if the identifier is a prop + // 1. is the key of the object property and doesn't have alias + // 2. is the item of the array pattern and doesn't have alias + // 3. is alias of the object property + const parentPath = path.parentPath; + if (parentPath.isObjectProperty() && path.parentKey === 'value') { + // collect alias of the object property + nestedProps.push(path.node.name); + } else if ( + parentPath.isArrayPattern() || + parentPath.isObjectPattern() || + parentPath.isRestElement() || + (parentPath.isAssignmentPattern() && path.key === 'left') + ) { + // collect the key of the object property or the item of the array pattern + nestedProps.push(path.node.name); + } + }, + }); + nestedRelationship = value; + } + addProp(current, PropType.SINGLE, key, defaultVal, alias, nestedProps, nestedRelationship); +} + +/** + * Analyze the props deconstructing in the function component + * 1. meet identifier, just collect the name + * 2. has alias, collect the alias name + * 3. has default value, collect the default value + * 4. has rest element, collect the rest element + * 5. nested destructuring, the e2e goal: + * ```js + * function(prop1, prop2: [p20, p21]) {} + * // transform into + * function({ prop1, prop2: [p20, p21] }) { + * let p20, p21 + * watch(() => { + * [p20, p21] = prop2 + * }) + * } + * ``` + */ +export function propsAnalyze(): Visitor { + return { + Prop(path: NodePath, ctx) { + if (path.isObjectProperty()) { + // --- 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; + analyzeSingleProp(value, name, path, ctx); + return; + } + + throw Error(`Unsupported key type in object destructuring: ${key.type}`); + } else { + // --- rest element --- + const arg = path.get('argument'); + if (!Array.isArray(arg) && arg.isIdentifier()) { + addProp(ctx.current, PropType.REST, arg.node.name); + } + } + }, + }; +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/types.ts b/packages/transpiler/babel-inula-next-core/src/analyze/types.ts index 24cf3439..752a849a 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/types.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/types.ts @@ -13,9 +13,9 @@ * See the Mulan PSL v2 for more details. */ -import { NodePath, types as t } from '@babel/core'; +import { type NodePath, types as t } from '@babel/core'; import { Node } from '@babel/traverse'; -import { ON_MOUNT, ON_UNMOUNT, WILL_MOUNT, WILL_UNMOUNT } from '../constants'; +import { ON_MOUNT, ON_UNMOUNT, PropType, WILL_MOUNT, WILL_UNMOUNT } from '../constants'; // --- Node shape --- export type InulaNode = ComponentNode | CondNode | JSXNode; @@ -23,35 +23,54 @@ export type JSX = t.JSXElement | t.JSXFragment; export type LifeCycle = typeof WILL_MOUNT | typeof ON_MOUNT | typeof WILL_UNMOUNT | typeof ON_UNMOUNT; type defaultVal = any | null; type Bitmap = number; -interface Reactive { +interface Property { name: string; value: t.Expression | null; // indicate the value is a state or computed or watch - listeners: string[]; - bitmap: Bitmap; + listeners?: string[]; + bitmap?: Bitmap; // need a flag for computed to gen a getter // watch is a static computed isComputed: boolean; + isMethod: boolean; +} +interface Prop { + name: string; + type: PropType; + alias: string | null; + default: t.Expression | null; + nestedProps: string[] | null; + nestedRelationship: t.ObjectPattern | t.ArrayPattern | null; } - export interface ComponentNode { type: 'comp'; name: string; - props: Record; - // A valuable could be a state or computed - valuable: Reactive; - methods: NodePath[]; + props: Prop[]; + // A properties could be a state or computed + properties: Property[]; + availableProperties: string[]; + /** + * The map to find the dependencies + */ + dependencyMap: { + [key: string]: string[]; + }; child?: InulaNode; - subComponents: ComponentNode[]; + subComponents?: ComponentNode[]; parent?: ComponentNode; /** * The function body of the fn component code */ - // fnBody: NodePath[]; - // a map to find the state + fnBody: NodePath[]; + /** + * The map to find the state + */ reactiveMap: Record; - level: number; - lifecycle: Record[][]>; + lifecycle: Partial[]>>; + watch?: { + deps: NodePath | null; + callback: NodePath | NodePath; + }[]; } export interface SubCompNode { @@ -84,19 +103,15 @@ export interface Branch { export interface AnalyzeContext { level: number; - index: number; - currentComponent: ComponentNode; - restStmt: NodePath[]; - // --- flow control --- - /** - * ignore the rest of the statements - */ - skipRest: () => void; + t: typeof t; + current: ComponentNode; traverse: (p: NodePath, ctx: AnalyzeContext) => void; } export type Visitor = { [Type in Node['type']]?: (path: NodePath>, state: S) => void; +} & { + Prop?: (path: NodePath, state: S) => void; }; export type Analyzer = () => Visitor; diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/viewAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/viewAnalyze.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/watchAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/watchAnalyze.ts new file mode 100644 index 00000000..1e05cf23 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/src/analyze/watchAnalyze.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { NodePath } from '@babel/core'; +import { Visitor } from './types'; +import { addWatch } from './nodeFactory'; +import * as t from '@babel/types'; +import { WATCH } from '../constants'; +import { extractFnFromMacro } from '../utils'; + +/** + * Analyze the watch in the function component + */ +export function watchAnalyze(): Visitor { + return { + ExpressionStatement(path: NodePath, ctx) { + const callExpression = path.get('expression'); + if (callExpression.isCallExpression()) { + const callee = callExpression.get('callee'); + if (callee.isIdentifier() && callee.node.name === WATCH) { + const fnNode = extractFnFromMacro(callExpression, WATCH); + const deps = getWatchDeps(callExpression); + addWatch(ctx.current, fnNode, deps); + } + } + }, + }; +} + +function getWatchDeps(callExpression: NodePath) { + const args = callExpression.get('arguments'); + if (!args[1]) { + return null; + } + + let deps: null | NodePath = null; + if (args[1].isArrayExpression()) { + deps = args[1]; + } else { + console.error('watch deps should be an array expression'); + } + return deps; +} diff --git a/packages/transpiler/babel-inula-next-core/src/babelTypes.ts b/packages/transpiler/babel-inula-next-core/src/babelTypes.ts new file mode 100644 index 00000000..f9420877 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/src/babelTypes.ts @@ -0,0 +1,22 @@ +import { type types as t } from '@babel/core'; +let _t: null | typeof types = null; + +export const register = (types: typeof t) => { + _t = types; +}; + +export const types = new Proxy( + {}, + { + get: (_, p, receiver) => { + if (!_t) { + throw new Error('Please call register() before using the babel types'); + } + + if (p in _t) { + return Reflect.get(_t, p, receiver); + } + return undefined; + }, + } +) as typeof t; diff --git a/packages/transpiler/babel-inula-next-core/src/constants.ts b/packages/transpiler/babel-inula-next-core/src/constants.ts index 51758c1b..6556fd3f 100644 --- a/packages/transpiler/babel-inula-next-core/src/constants.ts +++ b/packages/transpiler/babel-inula-next-core/src/constants.ts @@ -1,5 +1,29 @@ export const COMPONENT = 'Component'; export const WILL_MOUNT = 'willMount'; export const ON_MOUNT = 'onMount'; -export const WILL_UNMOUNT = 'willUnMount'; +export const WILL_UNMOUNT = 'willUnmount'; export const ON_UNMOUNT = 'onUnmount'; +export const WATCH = 'watch'; +export enum PropType { + REST = 'rest', + SINGLE = 'single', +} + +export const reactivityFuncNames = [ + // ---- Array + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse', + // ---- Set + 'add', + 'delete', + 'clear', + // ---- Map + 'set', + 'delete', + 'clear', +]; diff --git a/packages/transpiler/babel-inula-next-core/src/main.ts b/packages/transpiler/babel-inula-next-core/src/main.ts index 45ab3958..c9ab2b3c 100644 --- a/packages/transpiler/babel-inula-next-core/src/main.ts +++ b/packages/transpiler/babel-inula-next-core/src/main.ts @@ -1,12 +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 { 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; @@ -18,23 +18,16 @@ export default function (api: typeof babel, options: DLightOption): PluginObj { attributeMap = defaultAttributeMap, } = options; - const pluginProvider = new PluginProviderClass( - api, - types, - Array.isArray(files) ? files : [files], - Array.isArray(excludeFiles) ? excludeFiles : [excludeFiles], - enableDevTools, - htmlTags, - attributeMap - ); - + register(types); 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), }, CallExpression(path: NodePath) { // find the component, like: Component(() => {}) @@ -52,7 +45,7 @@ export default function (api: typeof babel, options: DLightOption): PluginObj { console.error('Component macro must be assigned to a variable'); } } - const root = analyze(name, componentNode); + const root = analyze(types, name, componentNode); // The sub path has been visited, so we just skip path.skip(); } diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/PropsFormatPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/PropsFormatPlugin.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/autoNamingPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/autoNamingPlugin.ts new file mode 100644 index 00000000..5de789bd --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/autoNamingPlugin.ts @@ -0,0 +1 @@ +// Auto Naming for Component and Hook diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/earlyReturnAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts similarity index 90% rename from packages/transpiler/babel-inula-next-core/src/analyze/earlyReturnAnalyze.ts rename to packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts index 842993d1..482554a8 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/earlyReturnAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts @@ -14,14 +14,14 @@ */ import { NodePath, type types as t } from '@babel/core'; -import { createComponentNode, createCondNode, createJSXNode } from './nodeFactory'; -import { AnalyzeContext, Branch, Visitor } from './types'; -import { isValidPath } from './utils'; +import { createComponentNode, createCondNode, createJSXNode } from '../analyze/nodeFactory'; +import { AnalyzeContext, Branch, Visitor } from '../analyze/types'; +import { isValidPath } from '../analyze/utils'; -export function earlyReturnAnalyze(): Visitor { +export function earlyReturnPlugin(): Visitor { return { ReturnStatement(path: NodePath, context: AnalyzeContext) { - const currentComp = context.currentComponent; + const currentComp = context.current; const argument = path.get('argument'); if (argument.isJSXElement()) { @@ -32,7 +32,7 @@ export function earlyReturnAnalyze(): Visitor { if (!hasEarlyReturn(ifStmt)) { return; } - const currentComp = context.currentComponent; + const currentComp = context.current; const branches: Branch[] = []; let next: NodePath | null = ifStmt; diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/forSubComponentPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/forSubComponentPlugin.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/jsxSliceAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts similarity index 64% rename from packages/transpiler/babel-inula-next-core/src/analyze/jsxSliceAnalyze.ts rename to packages/transpiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts index 0afdda3e..b1683e4b 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/jsxSliceAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts @@ -14,12 +14,12 @@ */ import { NodePath } from '@babel/core'; -import { AnalyzeContext, Visitor } from './types'; -import { createSubCompNode } from './nodeFactory'; +import { AnalyzeContext, Visitor } from '../analyze/types'; +import { createSubCompNode } from '../analyze/nodeFactory'; import * as t from '@babel/types'; function genName(tagName: string, ctx: AnalyzeContext) { - return `$$${tagName}-Sub${ctx.currentComponent.subComponents.length}`; + return `$$${tagName}-Sub${ctx.current.subComponents.length}`; } function genNameFromJSX(path: NodePath, ctx: AnalyzeContext) { @@ -33,8 +33,8 @@ function genNameFromJSX(path: NodePath, ctx: AnalyzeContext) { function replaceJSXSliceWithSubComp(name: string, ctx: AnalyzeContext, path: NodePath) { // create a subComponent node and add it to the current component - const subComp = createSubCompNode(name, ctx.currentComponent, path.node); - ctx.currentComponent.subComponents.push(subComp); + const subComp = createSubCompNode(name, ctx.current, path.node); + ctx.current.subComponents.push(subComp); // replace with the subComp jsxElement const subCompJSX = t.jsxElement( @@ -73,3 +73,28 @@ export function jsxSlicesAnalyze(): Visitor { }, }; } + +// Analyze the JSX slice in the function component, including: +// 1. VariableDeclaration, like `const a =
` +// 2. SubComponent, like `function Sub() { return
}` +function handleFn(fnName: string, fnBody: NodePath) { + if (isValidComponentName(fnName)) { + // This is a subcomponent, treat it as a normal component + } else { + // This is jsx creation function + // function jsxFunc() { + // // This is a function that returns JSX + // // because the function name is smallCamelCased + // return
{count}
+ // } + // => + // function jsxFunc() { + // function Comp_$id4$() { + // return
{count}
+ // } + // // This is a function that returns JSX + // // because the function name is smallCamelCased + // return + // } + } +} diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/transpiler/babel-inula-next-core/src/utils.ts b/packages/transpiler/babel-inula-next-core/src/utils.ts index d626967a..a4596b5e 100644 --- a/packages/transpiler/babel-inula-next-core/src/utils.ts +++ b/packages/transpiler/babel-inula-next-core/src/utils.ts @@ -18,5 +18,6 @@ export function getFnBody(path: NodePath).get('body'); + + return fnBody as unknown as NodePath; } 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 new file mode 100644 index 00000000..f063342b --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/analyze/lifeCycle.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { genCode, mockAnalyze } from '../mock'; +import { lifeCycleAnalyze } from '../../src/analyze/lifeCycleAnalyze'; +import { types } from '../../src/babelTypes'; +import { type NodePath, type types as t } from '@babel/core'; + +const analyze = (code: string) => mockAnalyze(code, [lifeCycleAnalyze]); +const combine = (body: NodePath[]) => types.program(body.map(path => path.node)); + +describe('analyze lifeCycle', () => { + it('should collect will mount', () => { + const root = analyze(/*js*/ ` + Component(() => { + willMount(() => { + console.log('test'); + }) + }) + `); + + expect(genCode(combine(root.lifecycle.willMount!))).toMatchInlineSnapshot(` + "{ + console.log('test'); + }" + `); + }); + + it('should collect on mount', () => { + const root = analyze(/*js*/ ` + Component(() => { + onMount(() => { + console.log('test'); + }) + }) + `); + + expect(genCode(combine(root.lifecycle.onMount!))).toMatchInlineSnapshot(` + "{ + console.log('test'); + }" + `); + }); + + it('should collect willUnmount', () => { + const root = analyze(/*js*/ ` + Component(() => { + willUnmount(() => { + console.log('test'); + }) + }) + `); + + expect(genCode(combine(root.lifecycle.willUnmount!))).toMatchInlineSnapshot(` + "{ + console.log('test'); + }" + `); + }); + + it('should collect onUnmount', () => { + const root = analyze(/*js*/ ` + Component(() => { + onUnmount(() => { + console.log('test'); + }) + }) + `); + + expect(genCode(combine(root.lifecycle.onUnmount!))).toMatchInlineSnapshot(` + "{ + console.log('test'); + }" + `); + }); + + it('should handle multiple lifecycle methods', () => { + const root = analyze(/*js*/ ` + Component(() => { + willMount(() => { + console.log('willMount'); + }) + onMount(() => { + console.log('onMount'); + }) + willUnmount(() => { + console.log('willUnmount'); + }) + onUnmount(() => { + console.log('onUnmount'); + }) + }) + `); + + expect(genCode(combine(root.lifecycle.willMount!))).toMatchInlineSnapshot(` + "{ + console.log('willMount'); + }" + `); + expect(genCode(combine(root.lifecycle.onMount!))).toMatchInlineSnapshot(` + "{ + console.log('onMount'); + }" + `); + expect(genCode(combine(root.lifecycle.willUnmount!))).toMatchInlineSnapshot(` + "{ + console.log('willUnmount'); + }" + `); + expect(genCode(combine(root.lifecycle.onUnmount!))).toMatchInlineSnapshot(` + "{ + console.log('onUnmount'); + }" + `); + }); +}); 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 new file mode 100644 index 00000000..e3e37cf9 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { genCode, mockAnalyze } from '../mock'; +import { propertiesAnalyze } from '../../src/analyze/propertiesAnalyze'; +import { propsAnalyze } from '../../src/analyze/propsAnalyze'; + +const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, propertiesAnalyze]); + +describe('analyze properties', () => { + it('should work', () => { + const root = analyze(` + Component(() => { + let foo = 1; + let bar = 1; + }) + `); + expect(root.properties.length).toBe(2); + }); + + describe('state dependency', () => { + it('should analyze dependency from state', () => { + const root = analyze(` + Component(() => { + let foo = 1; + let bar = foo; + }) + `); + expect(root.properties.length).toBe(2); + expect(root.properties[0].isComputed).toBe(false); + expect(genCode(root.properties[0].value)).toBe('1'); + expect(root.properties[1].isComputed).toBe(true); + expect(genCode(root.properties[1].value)).toBe('foo'); + expect(root.dependencyMap).toEqual({ bar: ['foo'] }); + }); + + it('should analyze dependency from state in different shape', () => { + const root = analyze(` + Component(() => { + let foo = 1; + let a = 1; + let b = 0; + let bar = { foo: foo ? a : b }; + }) + `); + expect(root.properties.length).toBe(4); + expect(root.properties[3].isComputed).toBe(true); + expect(genCode(root.properties[3].value)).toMatchInlineSnapshot(` + "{ + foo: foo ? a : b + }" + `); + expect(root.dependencyMap).toEqual({ bar: ['foo', 'a', 'b'] }); + }); + + it('should analyze dependency from props', () => { + const root = analyze(` + Component(({ foo }) => { + let bar = foo; + }) + `); + expect(root.properties.length).toBe(1); + expect(root.properties[0].isComputed).toBe(true); + expect(root.dependencyMap).toEqual({ bar: ['foo'] }); + }); + + it('should analyze dependency from nested props', () => { + const root = analyze(` + Component(({ foo: foo1, name: [first, last] }) => { + let bar = [foo1, first, last]; + }) + `); + expect(root.properties.length).toBe(1); + expect(root.properties[0].isComputed).toBe(true); + expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] }); + }); + + it('should not collect invalid dependency', () => { + const root = analyze(` + const cond = true + Component(() => { + let bar = cond ? count : window.innerWidth; + }) + `); + expect(root.properties.length).toBe(1); + expect(root.properties[0].isComputed).toBe(false); + expect(root.dependencyMap).toEqual({}); + }); + }); + + it('should collect method', () => { + const root = analyze(` + Component(() => { + let foo = 1; + const onClick = () => {}; + const onHover = function() { + onClick(foo) + }; + function onInput() {} + }) + `); + expect(root.properties.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']); + expect(root.properties[1].isMethod).toBe(true); + expect(root.properties[2].isMethod).toBe(true); + expect(root.dependencyMap).toMatchInlineSnapshot(`{}`); + }); +}); 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 new file mode 100644 index 00000000..85231b97 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/analyze/props.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { genCode, mockAnalyze } from '../mock'; +import { PropType } from '../../src/constants'; +import { propsAnalyze } from '../../src/analyze/propsAnalyze'; + +const analyze = (code: string) => mockAnalyze(code, [propsAnalyze]); + +describe('analyze props', () => { + it('should work', () => { + const root = analyze(/*js*/ ` + Component(({foo, bar}) => {}) + `); + expect(root.props.length).toBe(2); + }); + + it('should support default value', () => { + const root = analyze(/*js*/ ` + Component(({foo = 'default', bar = 123}) => {}) + `); + expect(root.props.length).toBe(2); + expect(root.props[0].name).toBe('foo'); + expect(root.props[1].name).toBe('bar'); + }); + + it('should support alias', () => { + const root = analyze(/*js*/ ` + Component(({'foo': renamed, bar: anotherName}) => {}) + `); + expect(root.props.length).toBe(2); + expect(root.props[0].name).toBe('foo'); + expect(root.props[0].alias).toBe('renamed'); + expect(root.props[1].name).toBe('bar'); + expect(root.props[1].alias).toBe('anotherName'); + }); + + it('should support nested props', () => { + const root = analyze(/*js*/ ` + Component(({foo: {nested1, nested2}, bar}) => {}) + `); + expect(root.props.length).toBe(2); + expect(root.props[0].name).toBe('foo'); + expect(root.props[0].nestedProps).toEqual(['nested1', 'nested2']); + expect(genCode(root.props[0].nestedRelationship)).toMatchInlineSnapshot(` + "{ + nested1, + nested2 + }" + `); + expect(root.props[1].name).toBe('bar'); + }); + + it('should support complex nested props', () => { + // language=js + const root = analyze(/*js*/ ` + Component(function ({ + prop1, prop2: {p2: [p20X = defaultVal, {p211, p212: p212X = defaultVal}, ...restArr], p3, ...restObj}} + ) {}); + `); + // we should collect prop1, p20X, p211, p212X, p3 + expect(root.props.length).toBe(2); + expect(root.props[0].name).toBe('prop1'); + expect(root.props[1].name).toBe('prop2'); + expect(root.props[1].nestedProps).toEqual(['p20X', 'p211', 'p212X', 'restArr', 'p3', 'restObj']); + expect(genCode(root.props[1].nestedRelationship)).toMatchInlineSnapshot(` + "{ + p2: [p20X = defaultVal, { + p211, + p212: p212X = defaultVal + }, ...restArr], + p3, + ...restObj + }" + `); + }); + + it('should support rest element', () => { + const root = analyze(/*js*/ ` + Component(({foo, ...rest}) => {}) + `); + expect(root.props.length).toBe(2); + expect(root.props[0].name).toBe('foo'); + expect(root.props[0].type).toBe(PropType.SINGLE); + expect(root.props[1].name).toBe('rest'); + expect(root.props[1].type).toBe(PropType.REST); + }); + + it('should support empty props', () => { + const root = analyze(/*js*/ ` + Component(() => {}) + `); + expect(root.props.length).toBe(0); + }); +}); 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 new file mode 100644 index 00000000..ea38ca80 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts @@ -0,0 +1,31 @@ +import { propsAnalyze } from '../../src/analyze/propsAnalyze'; +import { watchAnalyze } from '../../src/analyze/watchAnalyze'; +import { genCode, mockAnalyze } from '../mock'; +import { describe, expect, it } from 'vitest'; + +const analyze = (code: string) => mockAnalyze(code, [watchAnalyze]); + +describe('watchAnalyze', () => { + it('should analyze watch expressions', () => { + const root = analyze(/*js*/ ` + Comp(() => { + watch(() => { + // watch expression + }, [a, b]); + }) + `); + expect(root.watch).toHaveLength(1); + if (!root?.watch?.[0].callback) { + throw new Error('watch callback not found'); + } + expect(genCode(root.watch[0].callback.node)).toMatchInlineSnapshot(` + "() => { + // watch expression + }" + `); + if (!root.watch[0].deps) { + throw new Error('watch deps not found'); + } + expect(genCode(root.watch[0].deps.node)).toMatchInlineSnapshot('"[a, b]"'); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/condition.test.tsx b/packages/transpiler/babel-inula-next-core/test/condition.test.x.tsx similarity index 100% rename from packages/transpiler/babel-inula-next-core/test/condition.test.tsx rename to packages/transpiler/babel-inula-next-core/test/condition.test.x.tsx diff --git a/packages/transpiler/babel-inula-next-core/test/index.test.tsx b/packages/transpiler/babel-inula-next-core/test/index.test.tsx deleted file mode 100644 index b07ce5bc..00000000 --- a/packages/transpiler/babel-inula-next-core/test/index.test.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { transform } from './presets'; - -describe('fn2Class', () => { - it('should transform jsx', () => { - expect( - transform(` - @View - class A { - Body() { - return
- } - }`) - ).toMatchInlineSnapshot(` - "import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight"; - class A extends View { - Body() { - let $node0; - $node0 = $$createElement("div"); - return [$node0]; - } - }" - `); - }); - - it('should transform jsx with reactive', () => { - expect( - transform(` - @Main - @View - class A { - count = 1 - Body() { - return
this.count++}>{this.count}
- } - }`) - ).toMatchInlineSnapshot(` - "import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight"; - class A extends View { - count = 1; - $$count = 1; - Body() { - let $node0, $node1; - this._$update = $changed => { - if ($changed & 1) { - $node1 && $node1.update(() => this.count, [this.count]); - } - }; - $node0 = $$createElement("div"); - $$delegateEvent($node0, "click", () => this._$ud(this.count++, "count")); - $node1 = new $$ExpNode(this.count, [this.count]); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [$node0]; - } - } - $$render("main", A);" - `); - }); - - it('should transform fragment', () => { - expect( - transform(` - @View - class A { - Body() { - return <> -
- - } - }`) - ).toMatchInlineSnapshot(` - "import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight"; - class A extends View { - Body() { - let $node0; - $node0 = $$createElement("div"); - return [$node0]; - } - }" - `); - }); - - it('should transform function component', () => { - expect( - transform(` - function MyApp() { - let count = 0; - return
count++}>{count}
- }`) - ).toMatchInlineSnapshot(` - "import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight"; - class MyApp extends View { - count = 0; - $$count = 1; - Body() { - let $node0, $node1; - this._$update = $changed => { - if ($changed & 1) { - $node1 && $node1.update(() => this.count, [this.count]); - } - }; - $node0 = $$createElement("div"); - $$delegateEvent($node0, "click", () => this._$ud(this.count++, "count")); - $node1 = new $$ExpNode(this.count, [this.count]); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [$node0]; - } - }" - `); - }); - - it('should transform function component reactively', () => { - expect( - transform(` - function MyComp() { - let count = 0 - return <> -

Hello dlight fn, {count}

- -