feat: sub component
This commit is contained in:
parent
5427f13880
commit
f15b7d1a14
|
@ -5,9 +5,7 @@ import { createComponentNode } from './nodeFactory';
|
||||||
import { propertiesAnalyze } from './propertiesAnalyze';
|
import { propertiesAnalyze } from './propertiesAnalyze';
|
||||||
import { lifeCycleAnalyze } from './lifeCycleAnalyze';
|
import { lifeCycleAnalyze } from './lifeCycleAnalyze';
|
||||||
import { getFnBody } from '../utils';
|
import { getFnBody } from '../utils';
|
||||||
|
|
||||||
const builtinAnalyzers = [propsAnalyze, propertiesAnalyze, lifeCycleAnalyze];
|
const builtinAnalyzers = [propsAnalyze, propertiesAnalyze, lifeCycleAnalyze];
|
||||||
let analyzers: Analyzer[] = builtinAnalyzers;
|
|
||||||
|
|
||||||
export function isCondNode(node: any): node is CondNode {
|
export function isCondNode(node: any): node is CondNode {
|
||||||
return node && node.type === 'cond';
|
return node && node.type === 'cond';
|
||||||
|
@ -36,16 +34,17 @@ function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
||||||
|
|
||||||
// walk through the function component body
|
// walk through the function component body
|
||||||
export function analyzeFnComp(
|
export function analyzeFnComp(
|
||||||
types: typeof t,
|
|
||||||
fnNode: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
fnNode: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
||||||
componentNode: ComponentNode,
|
componentNode: ComponentNode,
|
||||||
|
{ htmlTags, analyzers }: { analyzers: Analyzer[]; htmlTags: string[] },
|
||||||
level = 0
|
level = 0
|
||||||
) {
|
) {
|
||||||
const visitor = mergeVisitor(...analyzers);
|
const visitor = mergeVisitor(...analyzers);
|
||||||
const context: AnalyzeContext = {
|
const context: AnalyzeContext = {
|
||||||
level,
|
level,
|
||||||
t: types,
|
|
||||||
current: componentNode,
|
current: componentNode,
|
||||||
|
htmlTags,
|
||||||
|
analyzers,
|
||||||
traverse: (path: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
traverse: (path: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
||||||
path.traverse(visitor, ctx);
|
path.traverse(visitor, ctx);
|
||||||
},
|
},
|
||||||
|
@ -94,17 +93,14 @@ export function analyzeFnComp(
|
||||||
* @param customAnalyzers
|
* @param customAnalyzers
|
||||||
*/
|
*/
|
||||||
export function analyze(
|
export function analyze(
|
||||||
types: typeof t,
|
|
||||||
fnName: string,
|
fnName: string,
|
||||||
path: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
path: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
||||||
customAnalyzers?: Analyzer[]
|
options: { customAnalyzers?: Analyzer[]; htmlTags: string[] }
|
||||||
) {
|
) {
|
||||||
if (customAnalyzers) {
|
const analyzers = options?.customAnalyzers ? options.customAnalyzers : builtinAnalyzers;
|
||||||
analyzers = customAnalyzers;
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = createComponentNode(fnName, path);
|
const root = createComponentNode(fnName, path);
|
||||||
analyzeFnComp(types, path, root);
|
analyzeFnComp(path, root, { analyzers, htmlTags: options.htmlTags });
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,45 +27,43 @@ export function createComponentNode(
|
||||||
name,
|
name,
|
||||||
props: [],
|
props: [],
|
||||||
child: undefined,
|
child: undefined,
|
||||||
subComponents: [],
|
|
||||||
properties: [],
|
properties: [],
|
||||||
dependencyMap: {},
|
dependencyMap: {},
|
||||||
reactiveMap: {},
|
reactiveMap: {},
|
||||||
lifecycle: {},
|
lifecycle: {},
|
||||||
parent,
|
parent,
|
||||||
// fnBody,
|
// 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() {
|
get availableProperties() {
|
||||||
return comp.properties
|
return [...comp.ownAvailableProperties, ...(comp.parent ? comp.parent.availableProperties : [])];
|
||||||
.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;
|
return comp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addProperty(
|
export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, isComputed: boolean) {
|
||||||
comp: ComponentNode,
|
comp.properties.push({ name, value, isComputed, isMethod: false });
|
||||||
name: string,
|
|
||||||
value: t.Expression | null,
|
|
||||||
isComputed: boolean,
|
|
||||||
isMethod = false
|
|
||||||
) {
|
|
||||||
comp.properties.push({ name, value, isComputed, isMethod });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addMethod(comp: ComponentNode, name: string, value: t.Expression | null) {
|
export function addMethod(comp: ComponentNode, name: string, value: t.Expression | null) {
|
||||||
comp.properties.push({ name, value, isComputed: false, isMethod: true });
|
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(
|
export function addProp(
|
||||||
comp: ComponentNode,
|
comp: ComponentNode,
|
||||||
type: PropType,
|
type: PropType,
|
||||||
|
|
|
@ -14,12 +14,19 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AnalyzeContext, Visitor } from './types';
|
import { AnalyzeContext, Visitor } from './types';
|
||||||
import { addLifecycle, addMethod, addProperty } from './nodeFactory';
|
import { addMethod, addProperty, createComponentNode } from './nodeFactory';
|
||||||
import { isValidPath } from './utils';
|
import { isValidPath } from './utils';
|
||||||
import { type types as t, type NodePath } from '@babel/core';
|
import { type types as t, type NodePath } from '@babel/core';
|
||||||
import { reactivityFuncNames } from '../const';
|
import { reactivityFuncNames } from '../const';
|
||||||
import { types } from '../babelTypes';
|
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 {
|
export function propertiesAnalyze(): Visitor {
|
||||||
return {
|
return {
|
||||||
VariableDeclaration(path: NodePath<t.VariableDeclaration>, ctx) {
|
VariableDeclaration(path: NodePath<t.VariableDeclaration>, ctx) {
|
||||||
|
@ -38,10 +45,28 @@ export function propertiesAnalyze(): Visitor {
|
||||||
const init = declaration.get('init');
|
const init = declaration.get('init');
|
||||||
let deps: string[] | null = null;
|
let deps: string[] | null = null;
|
||||||
if (isValidPath(init)) {
|
if (isValidPath(init)) {
|
||||||
|
// the property is a method
|
||||||
if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
|
if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
|
||||||
addMethod(ctx.current, id.node.name, init.node);
|
addMethod(ctx.current, id.node.name, init.node);
|
||||||
return;
|
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<t.ArrowFunctionExpression>
|
||||||
|
| NodePath<t.FunctionExpression>;
|
||||||
|
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);
|
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?.length);
|
||||||
|
|
|
@ -3,64 +3,6 @@ import { AnalyzeContext, Visitor } from './types';
|
||||||
import { addProp } from './nodeFactory';
|
import { addProp } from './nodeFactory';
|
||||||
import { PropType } from '../constants';
|
import { PropType } from '../constants';
|
||||||
import { types } from '../babelTypes';
|
import { types } from '../babelTypes';
|
||||||
function analyzeSingleProp(
|
|
||||||
value: t.ObjectProperty['value'],
|
|
||||||
key: string,
|
|
||||||
path: NodePath<t.ObjectProperty>,
|
|
||||||
{ 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
|
* 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<t.ObjectProperty>,
|
||||||
|
{ 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);
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
export type LifeCycle = typeof WILL_MOUNT | typeof ON_MOUNT | typeof WILL_UNMOUNT | typeof ON_UNMOUNT;
|
||||||
type defaultVal = any | null;
|
type defaultVal = any | null;
|
||||||
type Bitmap = number;
|
type Bitmap = number;
|
||||||
interface Property {
|
interface BaseProperty<V> {
|
||||||
name: string;
|
name: string;
|
||||||
value: t.Expression | null;
|
value: V;
|
||||||
// indicate the value is a state or computed or watch
|
|
||||||
listeners?: string[];
|
|
||||||
bitmap?: Bitmap;
|
|
||||||
// need a flag for computed to gen a getter
|
// need a flag for computed to gen a getter
|
||||||
// watch is a static computed
|
// watch is a static computed
|
||||||
isComputed: boolean;
|
isComputed: boolean;
|
||||||
isMethod: boolean;
|
isMethod: boolean;
|
||||||
}
|
}
|
||||||
|
interface Property extends BaseProperty<t.Expression | null> {
|
||||||
|
// indicate the value is a state or computed or watch
|
||||||
|
listeners?: string[];
|
||||||
|
bitmap?: Bitmap;
|
||||||
|
}
|
||||||
|
interface SubCompProperty extends BaseProperty<ComponentNode> {
|
||||||
|
isSubComp: true;
|
||||||
|
}
|
||||||
interface Prop {
|
interface Prop {
|
||||||
name: string;
|
name: string;
|
||||||
type: PropType;
|
type: PropType;
|
||||||
|
@ -47,7 +52,15 @@ export interface ComponentNode {
|
||||||
name: string;
|
name: string;
|
||||||
props: Prop[];
|
props: Prop[];
|
||||||
// A properties could be a state or computed
|
// 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[];
|
availableProperties: string[];
|
||||||
/**
|
/**
|
||||||
* The map to find the dependencies
|
* The map to find the dependencies
|
||||||
|
@ -56,7 +69,6 @@ export interface ComponentNode {
|
||||||
[key: string]: string[];
|
[key: string]: string[];
|
||||||
};
|
};
|
||||||
child?: InulaNode;
|
child?: InulaNode;
|
||||||
subComponents?: ComponentNode[];
|
|
||||||
parent?: ComponentNode;
|
parent?: ComponentNode;
|
||||||
/**
|
/**
|
||||||
* The function body of the fn component code
|
* The function body of the fn component code
|
||||||
|
@ -103,8 +115,9 @@ export interface Branch {
|
||||||
|
|
||||||
export interface AnalyzeContext {
|
export interface AnalyzeContext {
|
||||||
level: number;
|
level: number;
|
||||||
t: typeof t;
|
|
||||||
current: ComponentNode;
|
current: ComponentNode;
|
||||||
|
analyzers: Analyzer[];
|
||||||
|
htmlTags: string[];
|
||||||
traverse: (p: NodePath<t.Statement>, ctx: AnalyzeContext) => void;
|
traverse: (p: NodePath<t.Statement>, ctx: AnalyzeContext) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<t.ReturnStatement>, { 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,8 +1,17 @@
|
||||||
import { type types as t } from '@babel/core';
|
import { type types as t } from '@babel/core';
|
||||||
|
import type babel from '@babel/core';
|
||||||
let _t: null | typeof types = null;
|
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) => {
|
export const getBabelApi = (): typeof babel => {
|
||||||
_t = types;
|
if (!babelApi) {
|
||||||
|
throw new Error('Please call register() before using the babel api');
|
||||||
|
}
|
||||||
|
return babelApi;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const types = new Proxy(
|
export const types = new Proxy(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type babel from '@babel/core';
|
import type babel from '@babel/core';
|
||||||
import { type PluginObj } from '@babel/core';
|
import { type PluginObj } from '@babel/core';
|
||||||
import { type DLightOption } from './types';
|
import { type DLightOption } from './types';
|
||||||
import { defaultAttributeMap } from './const';
|
import { defaultAttributeMap, defaultHTMLTags } from './const';
|
||||||
import { analyze } from './analyze';
|
import { analyze } from './analyze';
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
import { NodePath, type types as t } from '@babel/core';
|
||||||
import { COMPONENT } from './constants';
|
import { COMPONENT } from './constants';
|
||||||
|
@ -14,11 +14,18 @@ export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
files = '**/*.{js,ts,jsx,tsx}',
|
files = '**/*.{js,ts,jsx,tsx}',
|
||||||
excludeFiles = '**/{dist,node_modules,lib}/*',
|
excludeFiles = '**/{dist,node_modules,lib}/*',
|
||||||
enableDevTools = false,
|
enableDevTools = false,
|
||||||
htmlTags = defaultHtmlTags => defaultHtmlTags,
|
customHtmlTags = defaultHtmlTags => defaultHtmlTags,
|
||||||
attributeMap = defaultAttributeMap,
|
attributeMap = defaultAttributeMap,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
register(types);
|
const htmlTags =
|
||||||
|
typeof customHtmlTags === 'function'
|
||||||
|
? customHtmlTags(defaultHTMLTags)
|
||||||
|
: customHtmlTags.includes('*')
|
||||||
|
? [...new Set([...defaultHTMLTags, ...customHtmlTags])].filter(tag => tag !== '*')
|
||||||
|
: customHtmlTags;
|
||||||
|
|
||||||
|
register(api);
|
||||||
return {
|
return {
|
||||||
visitor: {
|
visitor: {
|
||||||
Program: {
|
Program: {
|
||||||
|
@ -45,7 +52,9 @@ export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
console.error('Component macro must be assigned to a variable');
|
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
|
// The sub path has been visited, so we just skip
|
||||||
path.skip();
|
path.skip();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { describe, expect, it } from 'vitest';
|
||||||
import { genCode, mockAnalyze } from '../mock';
|
import { genCode, mockAnalyze } from '../mock';
|
||||||
import { propertiesAnalyze } from '../../src/analyze/propertiesAnalyze';
|
import { propertiesAnalyze } from '../../src/analyze/propertiesAnalyze';
|
||||||
import { propsAnalyze } from '../../src/analyze/propsAnalyze';
|
import { propsAnalyze } from '../../src/analyze/propsAnalyze';
|
||||||
|
import { ComponentNode } from '../../src/analyze/types';
|
||||||
|
|
||||||
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, propertiesAnalyze]);
|
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', () => {
|
it('should collect method', () => {
|
||||||
const root = analyze(`
|
const root = analyze(`
|
||||||
Component(() => {
|
Component(() => {
|
||||||
|
@ -115,6 +138,6 @@ describe('analyze properties', () => {
|
||||||
expect(root.properties.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']);
|
expect(root.properties.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']);
|
||||||
expect(root.properties[1].isMethod).toBe(true);
|
expect(root.properties[1].isMethod).toBe(true);
|
||||||
expect(root.properties[2].isMethod).toBe(true);
|
expect(root.properties[2].isMethod).toBe(true);
|
||||||
expect(root.dependencyMap).toMatchInlineSnapshot(`{}`);
|
expect(root.dependencyMap).toMatchInlineSnapshot('{}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 <div className={name}>{count}</div>
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
expect(true).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -14,12 +14,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Analyzer, ComponentNode, InulaNode } from '../src/analyze/types';
|
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 syntaxJSX from '@babel/plugin-syntax-jsx';
|
||||||
import { analyze } from '../src/analyze';
|
import { analyze } from '../src/analyze';
|
||||||
import generate from '@babel/generator';
|
import generate from '@babel/generator';
|
||||||
import * as t from '@babel/types';
|
import * as t from '@babel/types';
|
||||||
import { register } from '../src/babelTypes';
|
import { register } from '../src/babelTypes';
|
||||||
|
import { defaultHTMLTags } from '../src/const';
|
||||||
|
|
||||||
export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode {
|
export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode {
|
||||||
let root: ComponentNode | null = null;
|
let root: ComponentNode | null = null;
|
||||||
|
@ -27,17 +28,17 @@ export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode
|
||||||
plugins: [
|
plugins: [
|
||||||
syntaxJSX.default ?? syntaxJSX,
|
syntaxJSX.default ?? syntaxJSX,
|
||||||
function (api): PluginObj {
|
function (api): PluginObj {
|
||||||
register(api.types);
|
register(api);
|
||||||
return {
|
return {
|
||||||
visitor: {
|
visitor: {
|
||||||
FunctionExpression: path => {
|
FunctionExpression: path => {
|
||||||
root = analyze(api.types, 'test', path, analyzers);
|
root = analyze('test', path, { customAnalyzers: analyzers, htmlTags: defaultHTMLTags });
|
||||||
if (root) {
|
if (root) {
|
||||||
path.skip();
|
path.skip();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ArrowFunctionExpression: path => {
|
ArrowFunctionExpression: path => {
|
||||||
root = analyze(api.types, 'test', path, analyzers);
|
root = analyze('test', path, { customAnalyzers: analyzers, htmlTags: defaultHTMLTags });
|
||||||
if (root) {
|
if (root) {
|
||||||
path.skip();
|
path.skip();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue