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 b7da4b34..b6d690e6 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/index.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/index.ts @@ -5,9 +5,7 @@ import { createComponentNode } from './nodeFactory'; import { propertiesAnalyze } from './propertiesAnalyze'; import { lifeCycleAnalyze } from './lifeCycleAnalyze'; import { getFnBody } from '../utils'; - const builtinAnalyzers = [propsAnalyze, propertiesAnalyze, lifeCycleAnalyze]; -let analyzers: Analyzer[] = builtinAnalyzers; export function isCondNode(node: any): node is CondNode { return node && node.type === 'cond'; @@ -36,16 +34,17 @@ function mergeVisitor(...visitors: Analyzer[]): Visitor { // walk through the function component body export function analyzeFnComp( - types: typeof t, fnNode: NodePath, componentNode: ComponentNode, + { htmlTags, analyzers }: { analyzers: Analyzer[]; htmlTags: string[] }, level = 0 ) { const visitor = mergeVisitor(...analyzers); const context: AnalyzeContext = { level, - t: types, current: componentNode, + htmlTags, + analyzers, traverse: (path: NodePath, ctx: AnalyzeContext) => { path.traverse(visitor, ctx); }, @@ -94,17 +93,14 @@ export function analyzeFnComp( * @param customAnalyzers */ export function analyze( - types: typeof t, fnName: string, path: NodePath, - customAnalyzers?: Analyzer[] + options: { customAnalyzers?: Analyzer[]; htmlTags: string[] } ) { - if (customAnalyzers) { - analyzers = customAnalyzers; - } + const analyzers = options?.customAnalyzers ? options.customAnalyzers : builtinAnalyzers; const root = createComponentNode(fnName, path); - analyzeFnComp(types, path, root); + analyzeFnComp(path, root, { analyzers, htmlTags: options.htmlTags }); return root; } 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 bdbfe6a2..36dd5c93 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts @@ -27,45 +27,43 @@ export function createComponentNode( name, props: [], child: undefined, - subComponents: [], properties: [], dependencyMap: {}, reactiveMap: {}, lifecycle: {}, parent, // fnBody, + get availableProps() { + return comp.props + .map(({ name, nestedProps, alias }) => { + const nested = nestedProps ? nestedProps.map(name => name) : []; + return [alias ? alias : name, ...nested]; + }) + .flat(); + }, + get ownAvailableProperties() { + return [...comp.properties.filter(p => !p.isMethod).map(({ name }) => name), ...comp.availableProps]; + }, 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() - ); + return [...comp.ownAvailableProperties, ...(comp.parent ? comp.parent.availableProperties : [])]; }, }; return comp; } -export function addProperty( - comp: ComponentNode, - name: string, - value: t.Expression | null, - isComputed: boolean, - isMethod = false -) { - comp.properties.push({ name, value, isComputed, isMethod }); +export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, isComputed: boolean) { + comp.properties.push({ name, value, isComputed, isMethod: false }); } export function addMethod(comp: ComponentNode, name: string, value: t.Expression | null) { comp.properties.push({ name, value, isComputed: false, isMethod: true }); } +export function addSubComponent(comp: ComponentNode, subComp: ComponentNode, isComputed: boolean) { + comp.properties.push({ name: subComp.name, value: subComp, isSubComp: true, isComputed, isMethod: false }); +} + export function addProp( comp: ComponentNode, type: PropType, 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 d71a90e3..32e0ea81 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/propertiesAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/propertiesAnalyze.ts @@ -14,12 +14,19 @@ */ import { AnalyzeContext, Visitor } from './types'; -import { addLifecycle, addMethod, addProperty } from './nodeFactory'; +import { addMethod, addProperty, 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 { COMPONENT } from '../constants'; +import { analyzeFnComp } from '.'; +/** + * collect all properties and methods from the node + * and analyze the dependencies of the properties + * @returns + */ export function propertiesAnalyze(): Visitor { return { VariableDeclaration(path: NodePath, ctx) { @@ -38,10 +45,28 @@ export function propertiesAnalyze(): Visitor { const init = declaration.get('init'); let deps: string[] | null = null; if (isValidPath(init)) { + // the property is a method if (init.isArrowFunctionExpression() || init.isFunctionExpression()) { addMethod(ctx.current, id.node.name, init.node); return; } + // Should like Component(() => {}) + if ( + init.isCallExpression() && + init.get('callee').isIdentifier() && + (init.get('callee').node as t.Identifier).name === COMPONENT && + (init.get('arguments')[0].isFunctionExpression() || init.get('arguments')[0].isArrowFunctionExpression()) + ) { + const fnNode = init.get('arguments')[0] as + | NodePath + | NodePath; + const subComponent = createComponentNode(id.node.name, fnNode, ctx.current); + + analyzeFnComp(fnNode, subComponent, ctx); + deps = getDependenciesFromNode(id.node.name, init, ctx); + addProperty(ctx.current, id.node.name, subComponent, !!deps?.length); + return; + } deps = getDependenciesFromNode(id.node.name, init, ctx); } addProperty(ctx.current, id.node.name, init.node || null, !!deps?.length); 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 654dd95f..e6d1a448 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/propsAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/propsAnalyze.ts @@ -3,64 +3,6 @@ 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 @@ -104,3 +46,62 @@ export function propsAnalyze(): Visitor { }, }; } + +function analyzeSingleProp( + value: t.ObjectProperty['value'], + key: string, + path: NodePath, + { 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 (types.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)) { + // 2. handle default value case + const assignedName = value.left; + defaultVal = value.right; + if (types.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 (types.isObjectPattern(value) || types.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); +} 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 752a849a..c1988d9e 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/types.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/types.ts @@ -23,17 +23,22 @@ 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 Property { +interface BaseProperty { name: string; - value: t.Expression | null; - // indicate the value is a state or computed or watch - listeners?: string[]; - bitmap?: Bitmap; + value: V; // need a flag for computed to gen a getter // watch is a static computed isComputed: boolean; isMethod: boolean; } +interface Property extends BaseProperty { + // indicate the value is a state or computed or watch + listeners?: string[]; + bitmap?: Bitmap; +} +interface SubCompProperty extends BaseProperty { + isSubComp: true; +} interface Prop { name: string; type: PropType; @@ -47,7 +52,15 @@ export interface ComponentNode { name: string; props: Prop[]; // A properties could be a state or computed - properties: Property[]; + properties: (Property | SubCompProperty)[]; + /** + * The available props for the component, including the nested props + */ + availableProps: string[]; + /** + * The available properties for the component + */ + ownAvailableProperties: string[]; availableProperties: string[]; /** * The map to find the dependencies @@ -56,7 +69,6 @@ export interface ComponentNode { [key: string]: string[]; }; child?: InulaNode; - subComponents?: ComponentNode[]; parent?: ComponentNode; /** * The function body of the fn component code @@ -103,8 +115,9 @@ export interface Branch { export interface AnalyzeContext { level: number; - t: typeof t; current: ComponentNode; + analyzers: Analyzer[]; + htmlTags: string[]; traverse: (p: NodePath, ctx: AnalyzeContext) => void; } diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/viewAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/viewAnalyze.ts index e69de29b..ed76b9ba 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/viewAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/viewAnalyze.ts @@ -0,0 +1,45 @@ +/* + * 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 { 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 { parseReactivity } from '@openinula/reactivity-parser'; +import { reactivityFuncNames } from '../const'; + +/** + * Analyze the watch in the function component + */ +export function viewAnalyze(): Visitor { + return { + ReturnStatement(path: NodePath, { htmlTags, current }) { + const returnNode = path.get('argument'); + if (returnNode.isJSXElement() || returnNode.isJSXFragment()) { + const viewUnits = parseJSX(returnNode.node, { + babelApi: getBabelApi(), + htmlTags, + parseTemplate: false, + }); + const [viewParticles, usedPropertySet] = parseReactivity(viewUnits, { + babelApi: getBabelApi(), + availableProperties: current.availableProperties, + dependencyMap: current.dependencyMap, + reactivityFuncNames, + }); + } + }, + }; +} diff --git a/packages/transpiler/babel-inula-next-core/src/babelTypes.ts b/packages/transpiler/babel-inula-next-core/src/babelTypes.ts index f9420877..bb24c43d 100644 --- a/packages/transpiler/babel-inula-next-core/src/babelTypes.ts +++ b/packages/transpiler/babel-inula-next-core/src/babelTypes.ts @@ -1,8 +1,17 @@ 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) => { + babelApi = api; + _t = api.types; +}; -export const register = (types: typeof t) => { - _t = types; +export const getBabelApi = (): typeof babel => { + if (!babelApi) { + throw new Error('Please call register() before using the babel api'); + } + return babelApi; }; export const types = new Proxy( diff --git a/packages/transpiler/babel-inula-next-core/src/main.ts b/packages/transpiler/babel-inula-next-core/src/main.ts index c9ab2b3c..f01485bd 100644 --- a/packages/transpiler/babel-inula-next-core/src/main.ts +++ b/packages/transpiler/babel-inula-next-core/src/main.ts @@ -1,7 +1,7 @@ import type babel from '@babel/core'; import { type PluginObj } from '@babel/core'; import { type DLightOption } from './types'; -import { defaultAttributeMap } from './const'; +import { defaultAttributeMap, defaultHTMLTags } from './const'; import { analyze } from './analyze'; import { NodePath, type types as t } from '@babel/core'; import { COMPONENT } from './constants'; @@ -14,11 +14,18 @@ export default function (api: typeof babel, options: DLightOption): PluginObj { files = '**/*.{js,ts,jsx,tsx}', excludeFiles = '**/{dist,node_modules,lib}/*', enableDevTools = false, - htmlTags = defaultHtmlTags => defaultHtmlTags, + customHtmlTags = defaultHtmlTags => defaultHtmlTags, attributeMap = defaultAttributeMap, } = options; - register(types); + const htmlTags = + typeof customHtmlTags === 'function' + ? customHtmlTags(defaultHTMLTags) + : customHtmlTags.includes('*') + ? [...new Set([...defaultHTMLTags, ...customHtmlTags])].filter(tag => tag !== '*') + : customHtmlTags; + + register(api); return { visitor: { Program: { @@ -45,7 +52,9 @@ export default function (api: typeof babel, options: DLightOption): PluginObj { console.error('Component macro must be assigned to a variable'); } } - const root = analyze(types, name, componentNode); + 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/test/analyze/properties.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts index e3e37cf9..224528a5 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 @@ -17,6 +17,7 @@ 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'; const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, propertiesAnalyze]); @@ -101,6 +102,28 @@ describe('analyze properties', () => { }); }); + describe('subComponent', () => { + it('should analyze dependency from subComponent', () => { + const root = analyze(` + Component(() => { + let foo = 1; + const Sub = Component(() => { + let bar = foo; + }); + }) + `); + expect(root.properties.length).toBe(2); + expect(root.dependencyMap).toEqual({ Sub: ['foo'] }); + expect((root.properties[1].value as ComponentNode).dependencyMap).toMatchInlineSnapshot(` + { + "bar": [ + "foo", + ], + } + `); + }); + }); + it('should collect method', () => { const root = analyze(` Component(() => { @@ -115,6 +138,6 @@ describe('analyze properties', () => { 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(`{}`); + expect(root.dependencyMap).toMatchInlineSnapshot('{}'); }); }); 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 new file mode 100644 index 00000000..23698fa7 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts @@ -0,0 +1,19 @@ +import { propertiesAnalyze } from '../../src/analyze/propertiesAnalyze'; +import { propsAnalyze } from '../../src/analyze/propsAnalyze'; +import { viewAnalyze } from '../../src/analyze/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 root = analyze(/*js*/ ` + Comp(({name}) => { + let count = 11 + return
{count}
+ }) + `); + expect(true).toHaveLength(1); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/mock.ts b/packages/transpiler/babel-inula-next-core/test/mock.ts index 2e3db475..1ac853c2 100644 --- a/packages/transpiler/babel-inula-next-core/test/mock.ts +++ b/packages/transpiler/babel-inula-next-core/test/mock.ts @@ -14,12 +14,13 @@ */ import { Analyzer, ComponentNode, InulaNode } from '../src/analyze/types'; -import babel, { type PluginObj, transform as transformWithBabel } from '@babel/core'; +import { type PluginObj, transform as transformWithBabel } from '@babel/core'; import syntaxJSX from '@babel/plugin-syntax-jsx'; import { analyze } from '../src/analyze'; import generate from '@babel/generator'; import * as t from '@babel/types'; import { register } from '../src/babelTypes'; +import { defaultHTMLTags } from '../src/const'; export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode { let root: ComponentNode | null = null; @@ -27,17 +28,17 @@ export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode plugins: [ syntaxJSX.default ?? syntaxJSX, function (api): PluginObj { - register(api.types); + register(api); return { visitor: { FunctionExpression: path => { - root = analyze(api.types, 'test', path, analyzers); + root = analyze('test', path, { customAnalyzers: analyzers, htmlTags: defaultHTMLTags }); if (root) { path.skip(); } }, ArrowFunctionExpression: path => { - root = analyze(api.types, 'test', path, analyzers); + root = analyze('test', path, { customAnalyzers: analyzers, htmlTags: defaultHTMLTags }); if (root) { path.skip(); }