feat: analyze watch lifeCycle properties

This commit is contained in:
Hoikan 2024-04-24 16:21:40 +08:00
parent fcc734e05f
commit 5427f13880
28 changed files with 940 additions and 414 deletions

View File

@ -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<Visitor<AnalyzeContext>>((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<t.Statement>[], componentNode: ComponentNode, level = 0) {
export function analyzeFnComp(
types: typeof t,
fnNode: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
componentNode: ComponentNode,
level = 0
) {
const visitor = mergeVisitor(...analyzers);
const visit = (p: NodePath<t.Statement>, ctx: AnalyzeContext) => {
const context: AnalyzeContext = {
level,
t: types,
current: componentNode,
traverse: (path: NodePath<t.Statement>, 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<t.Statement>, 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<t.Statement>[], 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<t.FunctionExpression | t.ArrowFunctionExpression>,
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;
}

View File

@ -32,13 +32,16 @@ function isLifeCycleName(name: string): name is LifeCycle {
*/
export function lifeCycleAnalyze(): Visitor {
return {
CallExpression(path: NodePath<t.CallExpression>, 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<t.ExpressionStatement>, 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));
}
}
}
},

View File

@ -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<t.Statement>[],
fnNode: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
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<t.FunctionDeclaration>) {
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<t.Statement>[]) {
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<t.BlockStatement>) {
const compLifecycle = comp.lifecycle;
if (!compLifecycle[lifeCycle]) {
compLifecycle[lifeCycle] = [];
}
compLifecycle[lifeCycle].push(stmts);
compLifecycle[lifeCycle]!.push(block);
}
export function addWatch(
comp: ComponentNode,
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>,
deps: NodePath<t.ArrayExpression> | 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<JSX>): JSXNode {

View File

@ -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 = <div />`
// 2. SubComponent, like `function Sub() { return <div /> }`
function handleFn(fnName: string, fnBody: NodePath<t.BlockStatement>) {
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 <div>{count}</div>
// }
// =>
// function jsxFunc() {
// function Comp_$id4$() {
// return <div>{count}</div>
// }
// // This is a function that returns JSX
// // because the function name is smallCamelCased
// return <Comp_$id4$/>
// }
}
}
// 3. jsx creation function, like `function create() { return <div /> }`
export function propertiesAnalyze(): Visitor {
return {
VariableDeclaration(path: NodePath<t.VariableDeclaration>, 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 = () => <div />`
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<t.BlockStatement>);
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<t.FunctionDeclaration>, ctx) {
FunctionDeclaration(path: NodePath<t.FunctionDeclaration>, { 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<t.Expression | t.ClassDeclaration>,
{ current }: AnalyzeContext
) {
// ---- Deps: console.log(this.count)
const deps = new Set<string>();
// ---- Assign deps: this.count = 1 / this.count++
const assignDeps = new Set<string>();
const visitor = (innerPath: NodePath<t.Identifier>) => {
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)
);
}

View File

@ -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.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
* 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<t.ObjectProperty | t.RestElement>, 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);
}
}
},
};
}

View File

@ -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<string, defaultVal>;
// A valuable could be a state or computed
valuable: Reactive;
methods: NodePath<t.FunctionDeclaration>[];
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<t.Statement>[];
// a map to find the state
fnBody: NodePath<t.Statement>[];
/**
* The map to find the state
*/
reactiveMap: Record<string, Bitmap>;
level: number;
lifecycle: Record<LifeCycle, NodePath<t.Statement>[][]>;
lifecycle: Partial<Record<LifeCycle, NodePath<t.Statement>[]>>;
watch?: {
deps: NodePath<t.ArrayExpression> | null;
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>;
}[];
}
export interface SubCompNode {
@ -84,19 +103,15 @@ export interface Branch {
export interface AnalyzeContext {
level: number;
index: number;
currentComponent: ComponentNode;
restStmt: NodePath<t.Statement>[];
// --- flow control ---
/**
* ignore the rest of the statements
*/
skipRest: () => void;
t: typeof t;
current: ComponentNode;
traverse: (p: NodePath<t.Statement>, ctx: AnalyzeContext) => void;
}
export type Visitor<S = AnalyzeContext> = {
[Type in Node['type']]?: (path: NodePath<Extract<Node, { type: Type }>>, state: S) => void;
} & {
Prop?: (path: NodePath<t.ObjectProperty | t.RestElement>, state: S) => void;
};
export type Analyzer = () => Visitor;

View File

@ -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<t.ExpressionStatement>, 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<t.CallExpression>) {
const args = callExpression.get('arguments');
if (!args[1]) {
return null;
}
let deps: null | NodePath<t.ArrayExpression> = null;
if (args[1].isArrayExpression()) {
deps = args[1];
} else {
console.error('watch deps should be an array expression');
}
return deps;
}

View File

@ -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;

View File

@ -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',
];

View File

@ -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<t.CallExpression>) {
// 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();
}

View File

@ -0,0 +1 @@
// Auto Naming for Component and Hook

View File

@ -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<t.ReturnStatement>, 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<t.Statement> | null = ifStmt;

View File

@ -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<t.JSXElement>, ctx: AnalyzeContext) {
@ -33,8 +33,8 @@ function genNameFromJSX(path: NodePath<t.JSXElement>, ctx: AnalyzeContext) {
function replaceJSXSliceWithSubComp(name: string, ctx: AnalyzeContext, path: NodePath<t.JSXElement | t.JSXFragment>) {
// 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 = <div />`
// 2. SubComponent, like `function Sub() { return <div /> }`
function handleFn(fnName: string, fnBody: NodePath<types.BlockStatement>) {
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 <div>{count}</div>
// }
// =>
// function jsxFunc() {
// function Comp_$id4$() {
// return <div>{count}</div>
// }
// // This is a function that returns JSX
// // because the function name is smallCamelCased
// return <Comp_$id4$/>
// }
}
}

View File

@ -18,5 +18,6 @@ export function getFnBody(path: NodePath<t.FunctionExpression | t.ArrowFunctionE
// turn expression into block statement for consistency
fnBody.replaceWith(t.blockStatement([t.returnStatement(fnBody.node)]));
}
return (fnBody as NodePath<t.BlockStatement>).get('body');
return fnBody as unknown as NodePath<t.BlockStatement>;
}

View File

@ -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<t.Statement>[]) => 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');
}"
`);
});
});

View File

@ -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(`{}`);
});
});

View File

@ -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);
});
});

View File

@ -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]"');
});
});

View File

@ -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 <div></div>
}
}`)
).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 <div onClick={() => this.count++}>{this.count}</div>
}
}`)
).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 <>
<div></div>
</>
}
}`)
).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 <div onClick={() => count++}>{count}</div>
}`)
).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 <>
<h1>Hello dlight fn, {count}</h1>
<button onClick={() => count +=1}>Add</button>
<Button />
</>
}`)
).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 MyComp extends View {
count = 0;
$$count = 1;
Body() {
let $node0, $node1, $node2, $node3, $node4;
this._$update = $changed => {
if ($changed & 1) {
$node2 && $node2.update(() => this.count, [this.count]);
}
};
$node0 = $$createElement("h1");
$node1 = $$createTextNode("Hello dlight fn, ", []);
$$insertNode($node0, $node1, 0);
$node2 = new $$ExpNode(this.count, [this.count]);
$$insertNode($node0, $node2, 1);
$node0._$nodes = [$node1, $node2];
$node3 = $$createElement("button");
$$delegateEvent($node3, "click", () => this._$ud(this.count += 1, "count"));
$node3.textContent = "Add";
$node4 = new Button();
$node4._$init(null, null, null, null);
return [$node0, $node3, $node4];
}
}"
`);
});
it('should transform children props', () => {
expect(
transform(`
function App({ children}) {
return <h1>{children}</h1>
}
`)
).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 App extends View {
get children() {
return this._$children;
}
Body() {
let $node0, $node1;
$node0 = $$createElement("h1");
$node1 = new $$ExpNode(this.children, []);
$$insertNode($node0, $node1, 0);
$node0._$nodes = [$node1];
return [$node0];
}
}"
`);
});
it('should transform component composition', () => {
expect(
transform(`
function ArrayModification({name}) {
let arr = 1
return <section>
<div>{arr}</div>
</section>
}
function MyComp() {
return <>
<ArrayModification name="1" />
</>
}
`)
).toMatchInlineSnapshot(`
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
class ArrayModification extends View {
$p$name;
name;
arr = 1;
$$arr = 2;
Body() {
let $node0, $node1, $node2;
this._$update = $changed => {
if ($changed & 2) {
$node2 && $node2.update(() => this.arr, [this.arr]);
}
};
$node0 = ArrayModification.$t0.cloneNode(true);
$node1 = $node0.firstChild;
$node2 = new $$ExpNode(this.arr, [this.arr]);
$$insertNode($node1, $node2, 0);
return [$node0];
}
static $t0 = (() => {
let $node0, $node1;
$node0 = $$createElement("section");
$node1 = $$createElement("div");
$node0.appendChild($node1);
return $node0;
})();
}
class MyComp extends View {
Body() {
let $node0;
$node0 = new ArrayModification();
$node0._$init([["name", "1", []]], null, null, null);
return [$node0];
}
}"
`);
});
});

View File

@ -13,26 +13,34 @@
* See the Mulan PSL v2 for more details.
*/
import { 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 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';
export function mockAnalyze(code: string): ComponentNode {
export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode {
let root: ComponentNode | null = null;
transformWithBabel(code, {
plugins: [
syntaxJSX.default ?? syntaxJSX,
function (api: typeof babel): PluginObj {
const { types } = api;
function (api): PluginObj {
register(api.types);
return {
visitor: {
FunctionDeclaration: {
enter: path => {
root = analyze(path);
},
FunctionExpression: path => {
root = analyze(api.types, 'test', path, analyzers);
if (root) {
path.skip();
}
},
ArrowFunctionExpression: path => {
root = analyze(api.types, 'test', path, analyzers);
if (root) {
path.skip();
}
},
},
};

View File

@ -14,8 +14,8 @@
*/
import { describe, expect, it } from 'vitest';
import { isCondNode } from '../src/analyze';
import { mockAnalyze } from './mock';
import { isCondNode } from '../../src/analyze';
import { mockAnalyze } from '../mock';
describe('analyze early return', () => {
it('should work', () => {

View File

@ -14,7 +14,7 @@
*/
import { describe, expect, it } from 'vitest';
import { genCode, mockAnalyze } from './mock';
import { genCode, mockAnalyze } from '../mock';
import generate from '@babel/generator';
describe('propertiesAnalyze', () => {

1
vitest.workspace.ts Normal file
View File

@ -0,0 +1 @@
export default ['packages/*', 'packages/**/*'];