feat: init
This commit is contained in:
parent
a536958ad4
commit
fcc734e05f
|
@ -0,0 +1,14 @@
|
||||||
|
# babel-preset-inula-next
|
||||||
|
|
||||||
|
## 0.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- @openinula/class-transformer@0.0.2
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 2f9d373: feat: change babel import
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"name": "babel-preset-inula-next",
|
||||||
|
"version": "0.0.3",
|
||||||
|
"author": {
|
||||||
|
"name": "IanDx",
|
||||||
|
"email": "iandxssxx@gmail.com"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"dlight.js",
|
||||||
|
"babel-preset"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.cjs",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup --sourcemap",
|
||||||
|
"test": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/babel__core": "^7.20.5",
|
||||||
|
"@types/node": "^20.10.5",
|
||||||
|
"tsup": "^6.7.0",
|
||||||
|
"typescript": "^5.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.23.3",
|
||||||
|
"@babel/generator": "^7.23.6",
|
||||||
|
"@babel/parser": "^7.24.4",
|
||||||
|
"@babel/plugin-syntax-decorators": "^7.23.3",
|
||||||
|
"@babel/plugin-syntax-jsx": "7.16.7",
|
||||||
|
"@babel/plugin-syntax-typescript": "^7.23.3",
|
||||||
|
"@babel/traverse": "^7.24.1",
|
||||||
|
"@babel/types": "^7.24.0",
|
||||||
|
"@openinula/class-transformer": "workspace:*",
|
||||||
|
"@openinula/reactivity-parser": "workspace:*",
|
||||||
|
"@openinula/view-generator": "workspace:*",
|
||||||
|
"@openinula/view-parser": "workspace:*",
|
||||||
|
"@types/babel-types": "^7.0.15",
|
||||||
|
"@types/babel__generator": "^7.6.8",
|
||||||
|
"@types/babel__parser": "^7.1.1",
|
||||||
|
"@types/babel__traverse": "^7.6.8",
|
||||||
|
"jsx-view-parser": "workspace:*",
|
||||||
|
"minimatch": "^9.0.3",
|
||||||
|
"vitest": "^1.4.0"
|
||||||
|
},
|
||||||
|
"tsup": {
|
||||||
|
"entry": [
|
||||||
|
"src/index.ts"
|
||||||
|
],
|
||||||
|
"format": [
|
||||||
|
"cjs",
|
||||||
|
"esm"
|
||||||
|
],
|
||||||
|
"clean": true,
|
||||||
|
"dts": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* 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, type types as t } from '@babel/core';
|
||||||
|
import { createComponentNode, createCondNode, createJSXNode } from './nodeFactory';
|
||||||
|
import { AnalyzeContext, Branch, Visitor } from './types';
|
||||||
|
import { isValidPath } from './utils';
|
||||||
|
|
||||||
|
export function earlyReturnAnalyze(): Visitor {
|
||||||
|
return {
|
||||||
|
ReturnStatement(path: NodePath<t.ReturnStatement>, context: AnalyzeContext) {
|
||||||
|
const currentComp = context.currentComponent;
|
||||||
|
|
||||||
|
const argument = path.get('argument');
|
||||||
|
if (argument.isJSXElement()) {
|
||||||
|
currentComp.child = createJSXNode(currentComp, argument);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IfStatement(ifStmt: NodePath<t.IfStatement>, context: AnalyzeContext) {
|
||||||
|
if (!hasEarlyReturn(ifStmt)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentComp = context.currentComponent;
|
||||||
|
|
||||||
|
const branches: Branch[] = [];
|
||||||
|
let next: NodePath<t.Statement> | null = ifStmt;
|
||||||
|
let branchIdx = 0;
|
||||||
|
|
||||||
|
// Walk through the if-else chain to create branches
|
||||||
|
while (next && next.isIfStatement()) {
|
||||||
|
const nextConditions = [next.get('test')];
|
||||||
|
// gen id for branch with babel
|
||||||
|
const name = `$$branch-${branchIdx}`;
|
||||||
|
branches.push({
|
||||||
|
conditions: nextConditions,
|
||||||
|
content: createComponentNode(name, getStatements(ifStmt.get('consequent')), currentComp),
|
||||||
|
});
|
||||||
|
|
||||||
|
const elseBranch: NodePath<t.Statement | null | undefined> = next.get('alternate');
|
||||||
|
next = isValidPath(elseBranch) ? elseBranch : null;
|
||||||
|
branchIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time for the else branch
|
||||||
|
// We merge the else branch with the rest statements in fc body to form the children
|
||||||
|
const elseBranch = next ? getStatements(next) : [];
|
||||||
|
const defaultComponent = createComponentNode(
|
||||||
|
'$$branch-default',
|
||||||
|
elseBranch.concat(context.restStmt),
|
||||||
|
currentComp
|
||||||
|
);
|
||||||
|
context.skipRest();
|
||||||
|
|
||||||
|
currentComp.child = createCondNode(currentComp, defaultComponent, branches);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatements(next: NodePath<t.Statement>) {
|
||||||
|
return next.isBlockStatement() ? next.get('body') : [next];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasEarlyReturn(path: NodePath<t.Node>) {
|
||||||
|
let hasReturn = false;
|
||||||
|
path.traverse({
|
||||||
|
ReturnStatement(path: NodePath<t.ReturnStatement>) {
|
||||||
|
if (
|
||||||
|
path.parentPath.isFunctionDeclaration() ||
|
||||||
|
path.parentPath.isFunctionExpression() ||
|
||||||
|
path.parentPath.isArrowFunctionExpression()
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasReturn = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return hasReturn;
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { NodePath } from '@babel/core';
|
||||||
|
import { jsxSlicesAnalyze } from './jsxSliceAnalyze';
|
||||||
|
import { earlyReturnAnalyze } from './earlyReturnAnalyze';
|
||||||
|
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 { getFnBody } from '../utils';
|
||||||
|
const builtinAnalyzers = [jsxSlicesAnalyze, earlyReturnAnalyze, propertiesAnalyze];
|
||||||
|
let analyzers: Analyzer[] = builtinAnalyzers;
|
||||||
|
|
||||||
|
export function isCondNode(node: any): node is CondNode {
|
||||||
|
return node && node.type === 'cond';
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
||||||
|
return visitors.reduce((acc, cur) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
...cur(),
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk through the function component body
|
||||||
|
export function iterateFCBody(bodyStatements: NodePath<t.Statement>[], componentNode: ComponentNode, level = 0) {
|
||||||
|
const visitor = mergeVisitor(...analyzers);
|
||||||
|
const visit = (p: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (p.isReturnStatement()) {
|
||||||
|
visitor.ReturnStatement?.(p, context);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipRest) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The process of analyzing the component
|
||||||
|
* 1. identify the component
|
||||||
|
* 2. identify the jsx slice in the 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 path
|
||||||
|
* @param customAnalyzers
|
||||||
|
*/
|
||||||
|
export function analyze(
|
||||||
|
fnName: string,
|
||||||
|
path: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
||||||
|
customAnalyzers?: Analyzer[]
|
||||||
|
) {
|
||||||
|
if (customAnalyzers) {
|
||||||
|
analyzers = customAnalyzers;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = createComponentNode(fnName, getFnBody(path));
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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 { AnalyzeContext, Visitor } from './types';
|
||||||
|
import { createSubCompNode } from './nodeFactory';
|
||||||
|
import * as t from '@babel/types';
|
||||||
|
|
||||||
|
function genName(tagName: string, ctx: AnalyzeContext) {
|
||||||
|
return `$$${tagName}-Sub${ctx.currentComponent.subComponents.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function genNameFromJSX(path: NodePath<t.JSXElement>, ctx: AnalyzeContext) {
|
||||||
|
const tagId = path.get('openingElement').get('name');
|
||||||
|
if (tagId.isJSXIdentifier()) {
|
||||||
|
const jsxName = tagId.node.name;
|
||||||
|
return genName(jsxName, ctx);
|
||||||
|
}
|
||||||
|
throw new Error('JSXMemberExpression is not supported yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// replace with the subComp jsxElement
|
||||||
|
const subCompJSX = t.jsxElement(
|
||||||
|
t.jsxOpeningElement(t.jsxIdentifier(name), [], true),
|
||||||
|
t.jsxClosingElement(t.jsxIdentifier(name)),
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
path.replaceWith(subCompJSX);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze the JSX slice in the function component
|
||||||
|
* 1. VariableDeclaration, like `const a = <div />`
|
||||||
|
* 2. SubComponent, like `function Sub() { return <div /> }`
|
||||||
|
*
|
||||||
|
* i.e.
|
||||||
|
* ```jsx
|
||||||
|
* let jsxSlice = <div>{count}</div>
|
||||||
|
* // =>
|
||||||
|
* function Comp_$id$() {
|
||||||
|
* return <div>{count}</div>
|
||||||
|
* }
|
||||||
|
* let jsxSlice = <Comp_$id$/>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function jsxSlicesAnalyze(): Visitor {
|
||||||
|
return {
|
||||||
|
JSXElement(path: NodePath<t.JSXElement>, ctx) {
|
||||||
|
const name = genNameFromJSX(path, ctx);
|
||||||
|
replaceJSXSliceWithSubComp(name, ctx, path);
|
||||||
|
path.skip();
|
||||||
|
},
|
||||||
|
JSXFragment(path: NodePath<t.JSXFragment>, ctx) {
|
||||||
|
replaceJSXSliceWithSubComp('frag', ctx, path);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* 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 { LifeCycle, Visitor } from './types';
|
||||||
|
import { addLifecycle } from './nodeFactory';
|
||||||
|
import * as t from '@babel/types';
|
||||||
|
import { ON_MOUNT, ON_UNMOUNT, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
||||||
|
import { extractFnFromMacro, getFnBody } from '../utils';
|
||||||
|
|
||||||
|
function isLifeCycleName(name: string): name is LifeCycle {
|
||||||
|
return [WILL_MOUNT, ON_MOUNT, WILL_UNMOUNT, ON_UNMOUNT].includes(name);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Analyze the lifeCycle in the function component
|
||||||
|
* 1. willMount
|
||||||
|
* 2. onMount
|
||||||
|
* 3. willUnMount
|
||||||
|
* 4. onUnmount
|
||||||
|
*/
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* 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, type types as t } from '@babel/core';
|
||||||
|
import { Branch, ComponentNode, CondNode, InulaNode, JSX, JSXNode, LifeCycle, SubCompNode } from './types';
|
||||||
|
import { iterateFCBody } from './index';
|
||||||
|
|
||||||
|
export function createComponentNode(
|
||||||
|
name: string,
|
||||||
|
fnBody: NodePath<t.Statement>[],
|
||||||
|
parent?: ComponentNode
|
||||||
|
): ComponentNode {
|
||||||
|
const comp: ComponentNode = {
|
||||||
|
type: 'comp',
|
||||||
|
name,
|
||||||
|
props: {},
|
||||||
|
child: undefined,
|
||||||
|
subComponents: [],
|
||||||
|
methods: [],
|
||||||
|
state: [],
|
||||||
|
parent,
|
||||||
|
fnBody,
|
||||||
|
};
|
||||||
|
|
||||||
|
iterateFCBody(fnBody, comp);
|
||||||
|
|
||||||
|
return comp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addState(comp: ComponentNode, name: string, value: t.Expression | null) {
|
||||||
|
comp.state.push({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addMethod(comp: ComponentNode, method: NodePath<t.FunctionDeclaration>) {
|
||||||
|
comp.methods.push(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, stmts: NodePath<t.Statement>[]) {
|
||||||
|
const compLifecycle = comp.lifecycle;
|
||||||
|
if (!compLifecycle[lifeCycle]) {
|
||||||
|
compLifecycle[lifeCycle] = [];
|
||||||
|
}
|
||||||
|
compLifecycle[lifeCycle].push(stmts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createJSXNode(parent: ComponentNode, content: NodePath<JSX>): JSXNode {
|
||||||
|
return {
|
||||||
|
type: 'jsx',
|
||||||
|
parent,
|
||||||
|
child: content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCondNode(parent: ComponentNode, child: InulaNode, branches: Branch[]): CondNode {
|
||||||
|
return {
|
||||||
|
type: 'cond',
|
||||||
|
branches,
|
||||||
|
child,
|
||||||
|
parent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSubCompNode(name: string, parent: ComponentNode, child: JSX): SubCompNode {
|
||||||
|
return {
|
||||||
|
type: 'subComp',
|
||||||
|
name,
|
||||||
|
parent,
|
||||||
|
child,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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 { 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) {
|
||||||
|
const declarations = path.get('declarations');
|
||||||
|
// iterate the declarations
|
||||||
|
declarations.forEach(declaration => {
|
||||||
|
const id = declaration.get('id');
|
||||||
|
// handle destructuring
|
||||||
|
if (id.isObjectPattern()) {
|
||||||
|
throw new Error('Object destructuring is not supported yet');
|
||||||
|
} else if (id.isArrayPattern()) {
|
||||||
|
// TODO: handle array destructuring
|
||||||
|
throw new Error('Array destructuring is not supported yet');
|
||||||
|
} else if (id.isIdentifier()) {
|
||||||
|
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>);
|
||||||
|
}
|
||||||
|
// handle jsx slice
|
||||||
|
ctx.traverse(path, ctx);
|
||||||
|
}
|
||||||
|
addState(ctx.currentComponent, id.node.name, declaration.node.init || null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
FunctionDeclaration(path: NodePath<t.FunctionDeclaration>, ctx) {
|
||||||
|
const fnId = path.node.id;
|
||||||
|
if (!fnId) {
|
||||||
|
// This is an anonymous function, collect into lifecycle
|
||||||
|
//TODO
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasJSX(path)) {
|
||||||
|
// This is a normal function, collect into methods
|
||||||
|
addMethod(ctx.currentComponent, path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFn(fnId.name, path.get('body'));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* 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, types as t } from '@babel/core';
|
||||||
|
import { Node } from '@babel/traverse';
|
||||||
|
import { ON_MOUNT, ON_UNMOUNT, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
||||||
|
|
||||||
|
// --- Node shape ---
|
||||||
|
export type InulaNode = ComponentNode | CondNode | JSXNode;
|
||||||
|
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 {
|
||||||
|
name: string;
|
||||||
|
value: t.Expression | null;
|
||||||
|
// indicate the value is a state or computed or watch
|
||||||
|
listeners: string[];
|
||||||
|
bitmap: Bitmap;
|
||||||
|
// need a flag for computed to gen a getter
|
||||||
|
// watch is a static computed
|
||||||
|
isComputed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>[];
|
||||||
|
child?: InulaNode;
|
||||||
|
subComponents: ComponentNode[];
|
||||||
|
parent?: ComponentNode;
|
||||||
|
/**
|
||||||
|
* The function body of the fn component code
|
||||||
|
*/
|
||||||
|
// fnBody: NodePath<t.Statement>[];
|
||||||
|
// a map to find the state
|
||||||
|
reactiveMap: Record<string, Bitmap>;
|
||||||
|
level: number;
|
||||||
|
lifecycle: Record<LifeCycle, NodePath<t.Statement>[][]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubCompNode {
|
||||||
|
type: 'subComp';
|
||||||
|
name: string;
|
||||||
|
parent: ComponentNode;
|
||||||
|
child: JSX;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JSXNode {
|
||||||
|
type: 'jsx';
|
||||||
|
parent: ComponentNode;
|
||||||
|
child: NodePath<JSX>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CondNode {
|
||||||
|
type: 'cond';
|
||||||
|
branches: Branch[];
|
||||||
|
parent: ComponentNode;
|
||||||
|
/**
|
||||||
|
* The default branch
|
||||||
|
*/
|
||||||
|
child: InulaNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Branch {
|
||||||
|
conditions: NodePath<t.Expression>[];
|
||||||
|
content: InulaNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalyzeContext {
|
||||||
|
level: number;
|
||||||
|
index: number;
|
||||||
|
currentComponent: ComponentNode;
|
||||||
|
restStmt: NodePath<t.Statement>[];
|
||||||
|
// --- flow control ---
|
||||||
|
/**
|
||||||
|
* ignore the rest of the statements
|
||||||
|
*/
|
||||||
|
skipRest: () => void;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
export type Analyzer = () => Visitor;
|
||||||
|
|
||||||
|
export interface FnComponentDeclaration extends t.FunctionDeclaration {
|
||||||
|
id: t.Identifier;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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, type types as t } from '@babel/core';
|
||||||
|
import { FnComponentDeclaration } from './types';
|
||||||
|
|
||||||
|
export function isValidPath<T>(path: NodePath<T>): path is NodePath<Exclude<T, undefined | null>> {
|
||||||
|
return !!path.node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The component name must be UpperCamelCase
|
||||||
|
export function isValidComponent(node: t.FunctionDeclaration): node is FnComponentDeclaration {
|
||||||
|
// the first letter of the component name must be uppercase
|
||||||
|
return node.id ? isValidComponentName(node.id.name) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidComponentName(name: string) {
|
||||||
|
// the first letter of the component name must be uppercase
|
||||||
|
return /^[A-Z]/.test(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasJSX(path: NodePath<t.Node>) {
|
||||||
|
if (path.isJSXElement()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there is JSXElement in the children
|
||||||
|
let seen = false;
|
||||||
|
path.traverse({
|
||||||
|
JSXElement() {
|
||||||
|
seen = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return seen;
|
||||||
|
}
|
|
@ -0,0 +1,490 @@
|
||||||
|
export const devMode = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
export const alterAttributeMap = {
|
||||||
|
class: 'className',
|
||||||
|
for: 'htmlFor',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reactivityFuncNames = [
|
||||||
|
// ---- Array
|
||||||
|
'push',
|
||||||
|
'pop',
|
||||||
|
'shift',
|
||||||
|
'unshift',
|
||||||
|
'splice',
|
||||||
|
'sort',
|
||||||
|
'reverse',
|
||||||
|
// ---- Set
|
||||||
|
'add',
|
||||||
|
'delete',
|
||||||
|
'clear',
|
||||||
|
// ---- Map
|
||||||
|
'set',
|
||||||
|
'delete',
|
||||||
|
'clear',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const defaultHTMLTags = [
|
||||||
|
'a',
|
||||||
|
'abbr',
|
||||||
|
'address',
|
||||||
|
'area',
|
||||||
|
'article',
|
||||||
|
'aside',
|
||||||
|
'audio',
|
||||||
|
'b',
|
||||||
|
'base',
|
||||||
|
'bdi',
|
||||||
|
'bdo',
|
||||||
|
'blockquote',
|
||||||
|
'body',
|
||||||
|
'br',
|
||||||
|
'button',
|
||||||
|
'canvas',
|
||||||
|
'caption',
|
||||||
|
'cite',
|
||||||
|
'code',
|
||||||
|
'col',
|
||||||
|
'colgroup',
|
||||||
|
'data',
|
||||||
|
'datalist',
|
||||||
|
'dd',
|
||||||
|
'del',
|
||||||
|
'details',
|
||||||
|
'dfn',
|
||||||
|
'dialog',
|
||||||
|
'div',
|
||||||
|
'dl',
|
||||||
|
'dt',
|
||||||
|
'em',
|
||||||
|
'embed',
|
||||||
|
'fieldset',
|
||||||
|
'figcaption',
|
||||||
|
'figure',
|
||||||
|
'footer',
|
||||||
|
'form',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
'head',
|
||||||
|
'header',
|
||||||
|
'hgroup',
|
||||||
|
'hr',
|
||||||
|
'html',
|
||||||
|
'i',
|
||||||
|
'iframe',
|
||||||
|
'img',
|
||||||
|
'input',
|
||||||
|
'ins',
|
||||||
|
'kbd',
|
||||||
|
'label',
|
||||||
|
'legend',
|
||||||
|
'li',
|
||||||
|
'link',
|
||||||
|
'main',
|
||||||
|
'map',
|
||||||
|
'mark',
|
||||||
|
'menu',
|
||||||
|
'meta',
|
||||||
|
'meter',
|
||||||
|
'nav',
|
||||||
|
'noscript',
|
||||||
|
'object',
|
||||||
|
'ol',
|
||||||
|
'optgroup',
|
||||||
|
'option',
|
||||||
|
'output',
|
||||||
|
'p',
|
||||||
|
'picture',
|
||||||
|
'pre',
|
||||||
|
'progress',
|
||||||
|
'q',
|
||||||
|
'rp',
|
||||||
|
'rt',
|
||||||
|
'ruby',
|
||||||
|
's',
|
||||||
|
'samp',
|
||||||
|
'script',
|
||||||
|
'section',
|
||||||
|
'select',
|
||||||
|
'slot',
|
||||||
|
'small',
|
||||||
|
'source',
|
||||||
|
'span',
|
||||||
|
'strong',
|
||||||
|
'style',
|
||||||
|
'sub',
|
||||||
|
'summary',
|
||||||
|
'sup',
|
||||||
|
'table',
|
||||||
|
'tbody',
|
||||||
|
'td',
|
||||||
|
'template',
|
||||||
|
'textarea',
|
||||||
|
'tfoot',
|
||||||
|
'th',
|
||||||
|
'thead',
|
||||||
|
'time',
|
||||||
|
'title',
|
||||||
|
'tr',
|
||||||
|
'track',
|
||||||
|
'u',
|
||||||
|
'ul',
|
||||||
|
'var',
|
||||||
|
'video',
|
||||||
|
'wbr',
|
||||||
|
'acronym',
|
||||||
|
'applet',
|
||||||
|
'basefont',
|
||||||
|
'bgsound',
|
||||||
|
'big',
|
||||||
|
'blink',
|
||||||
|
'center',
|
||||||
|
'dir',
|
||||||
|
'font',
|
||||||
|
'frame',
|
||||||
|
'frameset',
|
||||||
|
'isindex',
|
||||||
|
'keygen',
|
||||||
|
'listing',
|
||||||
|
'marquee',
|
||||||
|
'menuitem',
|
||||||
|
'multicol',
|
||||||
|
'nextid',
|
||||||
|
'nobr',
|
||||||
|
'noembed',
|
||||||
|
'noframes',
|
||||||
|
'param',
|
||||||
|
'plaintext',
|
||||||
|
'rb',
|
||||||
|
'rtc',
|
||||||
|
'spacer',
|
||||||
|
'strike',
|
||||||
|
'tt',
|
||||||
|
'xmp',
|
||||||
|
'animate',
|
||||||
|
'animateMotion',
|
||||||
|
'animateTransform',
|
||||||
|
'circle',
|
||||||
|
'clipPath',
|
||||||
|
'defs',
|
||||||
|
'desc',
|
||||||
|
'ellipse',
|
||||||
|
'feBlend',
|
||||||
|
'feColorMatrix',
|
||||||
|
'feComponentTransfer',
|
||||||
|
'feComposite',
|
||||||
|
'feConvolveMatrix',
|
||||||
|
'feDiffuseLighting',
|
||||||
|
'feDisplacementMap',
|
||||||
|
'feDistantLight',
|
||||||
|
'feDropShadow',
|
||||||
|
'feFlood',
|
||||||
|
'feFuncA',
|
||||||
|
'feFuncB',
|
||||||
|
'feFuncG',
|
||||||
|
'feFuncR',
|
||||||
|
'feGaussianBlur',
|
||||||
|
'feImage',
|
||||||
|
'feMerge',
|
||||||
|
'feMergeNode',
|
||||||
|
'feMorphology',
|
||||||
|
'feOffset',
|
||||||
|
'fePointLight',
|
||||||
|
'feSpecularLighting',
|
||||||
|
'feSpotLight',
|
||||||
|
'feTile',
|
||||||
|
'feTurbulence',
|
||||||
|
'filter',
|
||||||
|
'foreignObject',
|
||||||
|
'g',
|
||||||
|
'image',
|
||||||
|
'line',
|
||||||
|
'linearGradient',
|
||||||
|
'marker',
|
||||||
|
'mask',
|
||||||
|
'metadata',
|
||||||
|
'mpath',
|
||||||
|
'path',
|
||||||
|
'pattern',
|
||||||
|
'polygon',
|
||||||
|
'polyline',
|
||||||
|
'radialGradient',
|
||||||
|
'rect',
|
||||||
|
'set',
|
||||||
|
'stop',
|
||||||
|
'svg',
|
||||||
|
'switch',
|
||||||
|
'symbol',
|
||||||
|
'text',
|
||||||
|
'textPath',
|
||||||
|
'tspan',
|
||||||
|
'use',
|
||||||
|
'view',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const availableDecoNames = ['Static', 'Prop', 'Env', 'Content', 'Children'];
|
||||||
|
export const dlightDefaultPackageName = '@openinula/next';
|
||||||
|
|
||||||
|
export const importMap = Object.fromEntries(
|
||||||
|
[
|
||||||
|
'createElement',
|
||||||
|
'setStyle',
|
||||||
|
'setDataset',
|
||||||
|
'setEvent',
|
||||||
|
'delegateEvent',
|
||||||
|
'setHTMLProp',
|
||||||
|
'setHTMLAttr',
|
||||||
|
'setHTMLProps',
|
||||||
|
'setHTMLAttrs',
|
||||||
|
'createTextNode',
|
||||||
|
'updateText',
|
||||||
|
'insertNode',
|
||||||
|
'ForNode',
|
||||||
|
'CondNode',
|
||||||
|
'ExpNode',
|
||||||
|
'EnvNode',
|
||||||
|
'TryNode',
|
||||||
|
'SnippetNode',
|
||||||
|
'PropView',
|
||||||
|
'render',
|
||||||
|
].map(name => [name, `$$${name}`])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const importsToDelete = [
|
||||||
|
'Static',
|
||||||
|
'Children',
|
||||||
|
'Content',
|
||||||
|
'Prop',
|
||||||
|
'Env',
|
||||||
|
'Watch',
|
||||||
|
'ForwardProps',
|
||||||
|
'Main',
|
||||||
|
'App',
|
||||||
|
'Mount',
|
||||||
|
'_',
|
||||||
|
'env',
|
||||||
|
'Snippet',
|
||||||
|
...defaultHTMLTags.filter(tag => tag !== 'use'),
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief HTML internal attribute map, can be accessed as js property
|
||||||
|
*/
|
||||||
|
export const defaultAttributeMap = {
|
||||||
|
// ---- Other property as attribute
|
||||||
|
textContent: ['*'],
|
||||||
|
innerHTML: ['*'],
|
||||||
|
// ---- Source: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes
|
||||||
|
accept: ['form', 'input'],
|
||||||
|
// ---- Original: accept-charset
|
||||||
|
acceptCharset: ['form'],
|
||||||
|
accesskey: ['*'],
|
||||||
|
action: ['form'],
|
||||||
|
align: ['caption', 'col', 'colgroup', 'hr', 'iframe', 'img', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr'],
|
||||||
|
allow: ['iframe'],
|
||||||
|
alt: ['area', 'img', 'input'],
|
||||||
|
async: ['script'],
|
||||||
|
autocapitalize: ['*'],
|
||||||
|
autocomplete: ['form', 'input', 'select', 'textarea'],
|
||||||
|
autofocus: ['button', 'input', 'select', 'textarea'],
|
||||||
|
autoplay: ['audio', 'video'],
|
||||||
|
background: ['body', 'table', 'td', 'th'],
|
||||||
|
// ---- Original: base
|
||||||
|
bgColor: ['body', 'col', 'colgroup', 'marquee', 'table', 'tbody', 'tfoot', 'td', 'th', 'tr'],
|
||||||
|
border: ['img', 'object', 'table'],
|
||||||
|
buffered: ['audio', 'video'],
|
||||||
|
capture: ['input'],
|
||||||
|
charset: ['meta'],
|
||||||
|
checked: ['input'],
|
||||||
|
cite: ['blockquote', 'del', 'ins', 'q'],
|
||||||
|
className: ['*'],
|
||||||
|
color: ['font', 'hr'],
|
||||||
|
cols: ['textarea'],
|
||||||
|
// ---- Original: colspan
|
||||||
|
colSpan: ['td', 'th'],
|
||||||
|
content: ['meta'],
|
||||||
|
// ---- Original: contenteditable
|
||||||
|
contentEditable: ['*'],
|
||||||
|
contextmenu: ['*'],
|
||||||
|
controls: ['audio', 'video'],
|
||||||
|
coords: ['area'],
|
||||||
|
crossOrigin: ['audio', 'img', 'link', 'script', 'video'],
|
||||||
|
csp: ['iframe'],
|
||||||
|
data: ['object'],
|
||||||
|
// ---- Original: datetime
|
||||||
|
dateTime: ['del', 'ins', 'time'],
|
||||||
|
decoding: ['img'],
|
||||||
|
default: ['track'],
|
||||||
|
defer: ['script'],
|
||||||
|
dir: ['*'],
|
||||||
|
dirname: ['input', 'textarea'],
|
||||||
|
disabled: ['button', 'fieldset', 'input', 'optgroup', 'option', 'select', 'textarea'],
|
||||||
|
download: ['a', 'area'],
|
||||||
|
draggable: ['*'],
|
||||||
|
enctype: ['form'],
|
||||||
|
// ---- Original: enterkeyhint
|
||||||
|
enterKeyHint: ['textarea', 'contenteditable'],
|
||||||
|
htmlFor: ['label', 'output'],
|
||||||
|
form: ['button', 'fieldset', 'input', 'label', 'meter', 'object', 'output', 'progress', 'select', 'textarea'],
|
||||||
|
// ---- Original: formaction
|
||||||
|
formAction: ['input', 'button'],
|
||||||
|
// ---- Original: formenctype
|
||||||
|
formEnctype: ['button', 'input'],
|
||||||
|
// ---- Original: formmethod
|
||||||
|
formMethod: ['button', 'input'],
|
||||||
|
// ---- Original: formnovalidate
|
||||||
|
formNoValidate: ['button', 'input'],
|
||||||
|
// ---- Original: formtarget
|
||||||
|
formTarget: ['button', 'input'],
|
||||||
|
headers: ['td', 'th'],
|
||||||
|
height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
||||||
|
hidden: ['*'],
|
||||||
|
high: ['meter'],
|
||||||
|
href: ['a', 'area', 'base', 'link'],
|
||||||
|
hreflang: ['a', 'link'],
|
||||||
|
// ---- Original: http-equiv
|
||||||
|
httpEquiv: ['meta'],
|
||||||
|
id: ['*'],
|
||||||
|
integrity: ['link', 'script'],
|
||||||
|
// ---- Original: intrinsicsize
|
||||||
|
intrinsicSize: ['img'],
|
||||||
|
// ---- Original: inputmode
|
||||||
|
inputMode: ['textarea', 'contenteditable'],
|
||||||
|
ismap: ['img'],
|
||||||
|
// ---- Original: itemprop
|
||||||
|
itemProp: ['*'],
|
||||||
|
kind: ['track'],
|
||||||
|
label: ['optgroup', 'option', 'track'],
|
||||||
|
lang: ['*'],
|
||||||
|
language: ['script'],
|
||||||
|
loading: ['img', 'iframe'],
|
||||||
|
list: ['input'],
|
||||||
|
loop: ['audio', 'marquee', 'video'],
|
||||||
|
low: ['meter'],
|
||||||
|
manifest: ['html'],
|
||||||
|
max: ['input', 'meter', 'progress'],
|
||||||
|
// ---- Original: maxlength
|
||||||
|
maxLength: ['input', 'textarea'],
|
||||||
|
// ---- Original: minlength
|
||||||
|
minLength: ['input', 'textarea'],
|
||||||
|
media: ['a', 'area', 'link', 'source', 'style'],
|
||||||
|
method: ['form'],
|
||||||
|
min: ['input', 'meter'],
|
||||||
|
multiple: ['input', 'select'],
|
||||||
|
muted: ['audio', 'video'],
|
||||||
|
name: [
|
||||||
|
'button',
|
||||||
|
'form',
|
||||||
|
'fieldset',
|
||||||
|
'iframe',
|
||||||
|
'input',
|
||||||
|
'object',
|
||||||
|
'output',
|
||||||
|
'select',
|
||||||
|
'textarea',
|
||||||
|
'map',
|
||||||
|
'meta',
|
||||||
|
'param',
|
||||||
|
],
|
||||||
|
// ---- Original: novalidate
|
||||||
|
noValidate: ['form'],
|
||||||
|
open: ['details', 'dialog'],
|
||||||
|
optimum: ['meter'],
|
||||||
|
pattern: ['input'],
|
||||||
|
ping: ['a', 'area'],
|
||||||
|
placeholder: ['input', 'textarea'],
|
||||||
|
// ---- Original: playsinline
|
||||||
|
playsInline: ['video'],
|
||||||
|
poster: ['video'],
|
||||||
|
preload: ['audio', 'video'],
|
||||||
|
readonly: ['input', 'textarea'],
|
||||||
|
// ---- Original: referrerpolicy
|
||||||
|
referrerPolicy: ['a', 'area', 'iframe', 'img', 'link', 'script'],
|
||||||
|
rel: ['a', 'area', 'link'],
|
||||||
|
required: ['input', 'select', 'textarea'],
|
||||||
|
reversed: ['ol'],
|
||||||
|
role: ['*'],
|
||||||
|
rows: ['textarea'],
|
||||||
|
// ---- Original: rowspan
|
||||||
|
rowSpan: ['td', 'th'],
|
||||||
|
sandbox: ['iframe'],
|
||||||
|
scope: ['th'],
|
||||||
|
scoped: ['style'],
|
||||||
|
selected: ['option'],
|
||||||
|
shape: ['a', 'area'],
|
||||||
|
size: ['input', 'select'],
|
||||||
|
sizes: ['link', 'img', 'source'],
|
||||||
|
slot: ['*'],
|
||||||
|
span: ['col', 'colgroup'],
|
||||||
|
spellcheck: ['*'],
|
||||||
|
src: ['audio', 'embed', 'iframe', 'img', 'input', 'script', 'source', 'track', 'video'],
|
||||||
|
srcdoc: ['iframe'],
|
||||||
|
srclang: ['track'],
|
||||||
|
srcset: ['img', 'source'],
|
||||||
|
start: ['ol'],
|
||||||
|
step: ['input'],
|
||||||
|
style: ['*'],
|
||||||
|
summary: ['table'],
|
||||||
|
// ---- Original: tabindex
|
||||||
|
tabIndex: ['*'],
|
||||||
|
target: ['a', 'area', 'base', 'form'],
|
||||||
|
title: ['*'],
|
||||||
|
translate: ['*'],
|
||||||
|
type: ['button', 'input', 'embed', 'object', 'ol', 'script', 'source', 'style', 'menu', 'link'],
|
||||||
|
usemap: ['img', 'input', 'object'],
|
||||||
|
value: ['button', 'data', 'input', 'li', 'meter', 'option', 'progress', 'param', 'text' /** extra for TextNode */],
|
||||||
|
width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
||||||
|
wrap: ['textarea'],
|
||||||
|
// --- ARIA attributes
|
||||||
|
// Source: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
|
||||||
|
ariaAutocomplete: ['*'],
|
||||||
|
ariaChecked: ['*'],
|
||||||
|
ariaDisabled: ['*'],
|
||||||
|
ariaErrorMessage: ['*'],
|
||||||
|
ariaExpanded: ['*'],
|
||||||
|
ariaHasPopup: ['*'],
|
||||||
|
ariaHidden: ['*'],
|
||||||
|
ariaInvalid: ['*'],
|
||||||
|
ariaLabel: ['*'],
|
||||||
|
ariaLevel: ['*'],
|
||||||
|
ariaModal: ['*'],
|
||||||
|
ariaMultiline: ['*'],
|
||||||
|
ariaMultiSelectable: ['*'],
|
||||||
|
ariaOrientation: ['*'],
|
||||||
|
ariaPlaceholder: ['*'],
|
||||||
|
ariaPressed: ['*'],
|
||||||
|
ariaReadonly: ['*'],
|
||||||
|
ariaRequired: ['*'],
|
||||||
|
ariaSelected: ['*'],
|
||||||
|
ariaSort: ['*'],
|
||||||
|
ariaValuemax: ['*'],
|
||||||
|
ariaValuemin: ['*'],
|
||||||
|
ariaValueNow: ['*'],
|
||||||
|
ariaValueText: ['*'],
|
||||||
|
ariaBusy: ['*'],
|
||||||
|
ariaLive: ['*'],
|
||||||
|
ariaRelevant: ['*'],
|
||||||
|
ariaAtomic: ['*'],
|
||||||
|
ariaDropEffect: ['*'],
|
||||||
|
ariaGrabbed: ['*'],
|
||||||
|
ariaActiveDescendant: ['*'],
|
||||||
|
ariaColCount: ['*'],
|
||||||
|
ariaColIndex: ['*'],
|
||||||
|
ariaColSpan: ['*'],
|
||||||
|
ariaControls: ['*'],
|
||||||
|
ariaDescribedBy: ['*'],
|
||||||
|
ariaDescription: ['*'],
|
||||||
|
ariaDetails: ['*'],
|
||||||
|
ariaFlowTo: ['*'],
|
||||||
|
ariaLabelledBy: ['*'],
|
||||||
|
ariaOwns: ['*'],
|
||||||
|
ariaPosInset: ['*'],
|
||||||
|
ariaRowCount: ['*'],
|
||||||
|
ariaRowIndex: ['*'],
|
||||||
|
ariaRowSpan: ['*'],
|
||||||
|
ariaSetSize: ['*'],
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
export const COMPONENT = 'Component';
|
||||||
|
export const WILL_MOUNT = 'willMount';
|
||||||
|
export const ON_MOUNT = 'onMount';
|
||||||
|
export const WILL_UNMOUNT = 'willUnMount';
|
||||||
|
export const ON_UNMOUNT = 'onUnmount';
|
|
@ -0,0 +1,4 @@
|
||||||
|
declare module '@babel/plugin-syntax-do-expressions';
|
||||||
|
declare module '@babel/plugin-syntax-decorators';
|
||||||
|
declare module '@babel/plugin-syntax-jsx';
|
||||||
|
declare module '@babel/plugin-syntax-typescript';
|
|
@ -0,0 +1,37 @@
|
||||||
|
import syntaxDecorators from '@babel/plugin-syntax-decorators';
|
||||||
|
import syntaxJSX from '@babel/plugin-syntax-jsx';
|
||||||
|
import syntaxTypescript from '@babel/plugin-syntax-typescript';
|
||||||
|
import dlight from './plugin';
|
||||||
|
import { type DLightOption } from './types';
|
||||||
|
import { type ConfigAPI, type TransformOptions } from '@babel/core';
|
||||||
|
import { plugin as fn2Class } from '@openinula/class-transformer';
|
||||||
|
import { parse as babelParse } from '@babel/parser';
|
||||||
|
|
||||||
|
export default function (_: ConfigAPI, options: DLightOption): TransformOptions {
|
||||||
|
return {
|
||||||
|
plugins: [
|
||||||
|
syntaxJSX.default ?? syntaxJSX,
|
||||||
|
[syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }],
|
||||||
|
[syntaxDecorators.default ?? syntaxDecorators, { legacy: true }],
|
||||||
|
fn2Class,
|
||||||
|
[dlight, options],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { type DLightOption };
|
||||||
|
|
||||||
|
export function parse(code: string) {
|
||||||
|
const result = babelParse(code, {
|
||||||
|
// parse in strict mode and allow module declarations
|
||||||
|
sourceType: 'module',
|
||||||
|
|
||||||
|
plugins: ['jsx'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.errors.length) {
|
||||||
|
throw new Error(result.errors[0].message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = result.program;
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
|
const { types } = api;
|
||||||
|
const {
|
||||||
|
files = '**/*.{js,ts,jsx,tsx}',
|
||||||
|
excludeFiles = '**/{dist,node_modules,lib}/*',
|
||||||
|
enableDevTools = false,
|
||||||
|
htmlTags = defaultHtmlTags => defaultHtmlTags,
|
||||||
|
attributeMap = defaultAttributeMap,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const pluginProvider = new PluginProviderClass(
|
||||||
|
api,
|
||||||
|
types,
|
||||||
|
Array.isArray(files) ? files : [files],
|
||||||
|
Array.isArray(excludeFiles) ? excludeFiles : [excludeFiles],
|
||||||
|
enableDevTools,
|
||||||
|
htmlTags,
|
||||||
|
attributeMap
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
Program: {
|
||||||
|
enter(path, { filename }) {
|
||||||
|
return pluginProvider.programEnterVisitor(path, filename);
|
||||||
|
},
|
||||||
|
exit: pluginProvider.programExitVisitor.bind(pluginProvider),
|
||||||
|
},
|
||||||
|
CallExpression(path: NodePath<t.CallExpression>) {
|
||||||
|
// find the component, like: Component(() => {})
|
||||||
|
const callee = path.get('callee');
|
||||||
|
|
||||||
|
if (callee.isIdentifier() && callee.node.name === COMPONENT) {
|
||||||
|
const componentNode = extractFnFromMacro(path, COMPONENT);
|
||||||
|
let name = '';
|
||||||
|
// try to get the component name, when parent is a variable declarator
|
||||||
|
if (path.parentPath.isVariableDeclarator()) {
|
||||||
|
const lVal = path.parentPath.get('id');
|
||||||
|
if (lVal.isIdentifier()) {
|
||||||
|
name = lVal.node.name;
|
||||||
|
} else {
|
||||||
|
console.error('Component macro must be assigned to a variable');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const root = analyze(name, componentNode);
|
||||||
|
// The sub path has been visited, so we just skip
|
||||||
|
path.skip();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
|
const { types } = api;
|
||||||
|
const {
|
||||||
|
files = '**/*.{js,ts,jsx,tsx}',
|
||||||
|
excludeFiles = '**/{dist,node_modules,lib}/*',
|
||||||
|
enableDevTools = false,
|
||||||
|
htmlTags = defaultHtmlTags => defaultHtmlTags,
|
||||||
|
attributeMap = defaultAttributeMap,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const pluginProvider = new PluginProviderClass(
|
||||||
|
api,
|
||||||
|
types,
|
||||||
|
Array.isArray(files) ? files : [files],
|
||||||
|
Array.isArray(excludeFiles) ? excludeFiles : [excludeFiles],
|
||||||
|
enableDevTools,
|
||||||
|
htmlTags,
|
||||||
|
attributeMap
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
Program: {
|
||||||
|
enter(path, { filename }) {
|
||||||
|
return pluginProvider.programEnterVisitor(path, filename);
|
||||||
|
},
|
||||||
|
exit: pluginProvider.programExitVisitor.bind(pluginProvider),
|
||||||
|
},
|
||||||
|
ClassDeclaration: {
|
||||||
|
enter: pluginProvider.classEnter.bind(pluginProvider),
|
||||||
|
exit: pluginProvider.classExit.bind(pluginProvider),
|
||||||
|
},
|
||||||
|
ClassMethod: pluginProvider.classMethodVisitor.bind(pluginProvider),
|
||||||
|
ClassProperty: pluginProvider.classPropertyVisitor.bind(pluginProvider),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,53 @@
|
||||||
|
import { type types as t } from '@babel/core';
|
||||||
|
|
||||||
|
export type HTMLTags = string[] | ((defaultHtmlTags: string[]) => string[]);
|
||||||
|
export interface DLightOption {
|
||||||
|
/**
|
||||||
|
* Files that will be included
|
||||||
|
* @default ** /*.{js,jsx,ts,tsx}
|
||||||
|
*/
|
||||||
|
files?: string | string[];
|
||||||
|
/**
|
||||||
|
* Files that will be excludes
|
||||||
|
* @default ** /{dist,node_modules,lib}/*.{js,ts}
|
||||||
|
*/
|
||||||
|
excludeFiles?: string | string[];
|
||||||
|
/**
|
||||||
|
* Enable devtools
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
enableDevTools?: boolean;
|
||||||
|
/**
|
||||||
|
* Custom HTML tags.
|
||||||
|
* Accepts 2 types:
|
||||||
|
* 1. string[], e.g. ["div", "span"]
|
||||||
|
* if contains "*", then all default tags will be included
|
||||||
|
* 2. (defaultHtmlTags: string[]) => string[]
|
||||||
|
* @default defaultHtmlTags => defaultHtmlTags
|
||||||
|
*/
|
||||||
|
htmlTags?: HTMLTags;
|
||||||
|
/**
|
||||||
|
* Allowed HTML tags from attributes
|
||||||
|
* e.g. { alt: ["area", "img", "input"] }
|
||||||
|
*/
|
||||||
|
attributeMap?: Record<string, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PropertyContainer = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
node: t.ClassProperty | t.ClassMethod;
|
||||||
|
deps: string[];
|
||||||
|
isStatic?: boolean;
|
||||||
|
isContent?: boolean;
|
||||||
|
isChildren?: boolean | number;
|
||||||
|
isModel?: boolean;
|
||||||
|
isWatcher?: boolean;
|
||||||
|
isPropOrEnv?: 'Prop' | 'Env';
|
||||||
|
depsNode?: t.ArrayExpression;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type IdentifierToDepNode = t.SpreadElement | t.Expression;
|
||||||
|
|
||||||
|
export type SnippetPropSubDepMap = Record<string, Record<string, string[]>>;
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { NodePath } from '@babel/core';
|
||||||
|
import * as t from '@babel/types';
|
||||||
|
|
||||||
|
export function extractFnFromMacro(path: NodePath<t.CallExpression>, macroName: string) {
|
||||||
|
const args = path.get('arguments');
|
||||||
|
|
||||||
|
const fnNode = args[0];
|
||||||
|
if (fnNode.isFunctionExpression() || fnNode.isArrowFunctionExpression()) {
|
||||||
|
return fnNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`${macroName} macro must have a function argument`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFnBody(path: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>) {
|
||||||
|
const fnBody = path.get('body');
|
||||||
|
if (fnBody.isExpression()) {
|
||||||
|
// turn expression into block statement for consistency
|
||||||
|
fnBody.replaceWith(t.blockStatement([t.returnStatement(fnBody.node)]));
|
||||||
|
}
|
||||||
|
return (fnBody as NodePath<t.BlockStatement>).get('body');
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* 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 { transform } from './presets';
|
||||||
|
|
||||||
|
describe('condition', () => {
|
||||||
|
it('should transform jsx', () => {
|
||||||
|
expect(
|
||||||
|
transform(`
|
||||||
|
function App() {
|
||||||
|
return <div>
|
||||||
|
<if cond={count > 1}>{count} is bigger than is 1</if>
|
||||||
|
<else>{count} is smaller than 1</else>
|
||||||
|
</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 "@openinula/next";
|
||||||
|
class App extends View {
|
||||||
|
Body() {
|
||||||
|
let $node0, $node1;
|
||||||
|
this._$update = $changed => {
|
||||||
|
$node1 && $node1.update($changed);
|
||||||
|
};
|
||||||
|
$node0 = $$createElement("div");
|
||||||
|
$node1 = new $$CondNode(0, $thisCond => {
|
||||||
|
if (count > 1) {
|
||||||
|
if ($thisCond.cond === 0) {
|
||||||
|
$thisCond.didntChange = true;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$thisCond.cond = 0;
|
||||||
|
let $node0, $node1;
|
||||||
|
$thisCond.updateFunc = $changed => {};
|
||||||
|
$node0 = new $$ExpNode(count, []);
|
||||||
|
$node1 = $$createTextNode(" is bigger than is 1", []);
|
||||||
|
return $thisCond.cond === 0 ? [$node0, $node1] : $thisCond.updateCond();
|
||||||
|
} else {
|
||||||
|
if ($thisCond.cond === 1) {
|
||||||
|
$thisCond.didntChange = true;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$thisCond.cond = 1;
|
||||||
|
let $node0, $node1;
|
||||||
|
$thisCond.updateFunc = $changed => {};
|
||||||
|
$node0 = new $$ExpNode(count, []);
|
||||||
|
$node1 = $$createTextNode(" is smaller than 1", []);
|
||||||
|
return $thisCond.cond === 1 ? [$node0, $node1] : $thisCond.updateCond();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$$insertNode($node0, $node1, 0);
|
||||||
|
$node0._$nodes = [$node1];
|
||||||
|
return [$node0];
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* 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 { isCondNode } from '../src/analyze';
|
||||||
|
import { mockAnalyze } from './mock';
|
||||||
|
|
||||||
|
describe('analyze early return', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
if (count > 1) {
|
||||||
|
return <div>1</div>
|
||||||
|
}
|
||||||
|
return <div>
|
||||||
|
<if cond={count > 1}>{count} is bigger than is 1</if>
|
||||||
|
<else>{count} is smaller than 1</else>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const branchNode = root?.child;
|
||||||
|
if (!isCondNode(branchNode)) {
|
||||||
|
throw new Error('Should be branch node');
|
||||||
|
}
|
||||||
|
expect(branchNode.branches.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with multi if', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
if (count > 1) {
|
||||||
|
return <div>1</div>
|
||||||
|
}
|
||||||
|
if (count > 2) {
|
||||||
|
return <div>2</div>
|
||||||
|
}
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const branchNode = root?.child;
|
||||||
|
if (!isCondNode(branchNode)) {
|
||||||
|
throw new Error('Should be branch node');
|
||||||
|
}
|
||||||
|
expect(branchNode.branches.length).toBe(1);
|
||||||
|
const subBranch = branchNode.child.child;
|
||||||
|
if (!isCondNode(subBranch)) {
|
||||||
|
throw new Error('SubBranchNode should be branch node');
|
||||||
|
}
|
||||||
|
expect(subBranch.branches.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with nested if', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
if (count > 1) {
|
||||||
|
if (count > 2) {
|
||||||
|
return <div>2</div>
|
||||||
|
}
|
||||||
|
return <div>1</div>
|
||||||
|
}
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const branchNode = root?.child;
|
||||||
|
if (!isCondNode(branchNode)) {
|
||||||
|
throw new Error('Should be branch node');
|
||||||
|
}
|
||||||
|
expect(branchNode.branches.length).toBe(1);
|
||||||
|
const subBranchNode = branchNode.branches[0].content.child;
|
||||||
|
if (!isCondNode(subBranchNode)) {
|
||||||
|
throw new Error('SubBranchNode should be branch node');
|
||||||
|
}
|
||||||
|
expect(subBranchNode.branches.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,233 @@
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* 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 { 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';
|
||||||
|
|
||||||
|
export function mockAnalyze(code: string): ComponentNode {
|
||||||
|
let root: ComponentNode | null = null;
|
||||||
|
transformWithBabel(code, {
|
||||||
|
plugins: [
|
||||||
|
syntaxJSX.default ?? syntaxJSX,
|
||||||
|
function (api: typeof babel): PluginObj {
|
||||||
|
const { types } = api;
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
FunctionDeclaration: {
|
||||||
|
enter: path => {
|
||||||
|
root = analyze(path);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
|
filename: 'test.tsx',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
throw new Error('root is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genCode(ast: t.Node | null) {
|
||||||
|
if (!ast) {
|
||||||
|
throw new Error('ast is null');
|
||||||
|
}
|
||||||
|
return generate(ast).code;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printTree(node: InulaNode | undefined): any {
|
||||||
|
if (!node) {
|
||||||
|
return 'empty';
|
||||||
|
}
|
||||||
|
if (node.type === 'cond') {
|
||||||
|
return {
|
||||||
|
type: node.type,
|
||||||
|
branch: node.branches.map(b => printTree(b.content)),
|
||||||
|
children: printTree(node.child),
|
||||||
|
};
|
||||||
|
} else if (node.type === 'comp') {
|
||||||
|
return {
|
||||||
|
type: node.type,
|
||||||
|
children: printTree(node.child),
|
||||||
|
};
|
||||||
|
} else if (node.type === 'jsx') {
|
||||||
|
return {
|
||||||
|
type: node.type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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 plugin from '../dist';
|
||||||
|
import { transform as transformWithBabel } from '@babel/core';
|
||||||
|
|
||||||
|
export function transform(code: string) {
|
||||||
|
return transformWithBabel(code, {
|
||||||
|
presets: [plugin],
|
||||||
|
filename: 'test.tsx',
|
||||||
|
})?.code;
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* 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 generate from '@babel/generator';
|
||||||
|
|
||||||
|
describe('propertiesAnalyze', () => {
|
||||||
|
describe('state', () => {
|
||||||
|
it('should work with jsx slice', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
const a = <div></div>
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(root.state[0].name).toBe('a');
|
||||||
|
expect(genCode(root.state[0].value)).toMatchInlineSnapshot(`"<$$div-Sub0 />"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with jsx slice in ternary operator', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
const a = true ? <div></div> : <h1></h1>
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(root.state[0].name).toBe('a');
|
||||||
|
expect(root.subComponents[0].name).toBe('$$div-Sub0');
|
||||||
|
expect(genCode(root.subComponents[0].child)).toMatchInlineSnapshot(`"<div></div>"`);
|
||||||
|
expect(root.subComponents[1].name).toBe('$$h1-Sub1');
|
||||||
|
expect(genCode(root.subComponents[1].child)).toMatchInlineSnapshot(`"<h1></h1>"`);
|
||||||
|
expect(genCode(root.state[0].value)).toMatchInlineSnapshot(`"true ? <$$div-Sub0 /> : <$$h1-Sub1 />"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with jsx slice in arr', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
const arr = [<div></div>,<h1></h1>]
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(root.state[0].name).toBe('a');
|
||||||
|
expect(root.subComponents[0].name).toBe('$$div-Sub0');
|
||||||
|
expect(genCode(root.subComponents[0].child)).toMatchInlineSnapshot(`"<div></div>"`);
|
||||||
|
expect(root.subComponents[1].name).toBe('$$h1-Sub1');
|
||||||
|
expect(genCode(root.subComponents[1].child)).toMatchInlineSnapshot(`"<h1></h1>"`);
|
||||||
|
expect(genCode(root.state[0].value)).toMatchInlineSnapshot(`"true ? <$$div-Sub0 /> : <$$h1-Sub1 />"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"esm": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@
|
||||||
"typings": "dist/index.d.ts",
|
"typings": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup --sourcemap",
|
"build": "tsup --sourcemap",
|
||||||
"test": "vitest --ui"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/babel__core": "^7.20.5",
|
"@types/babel__core": "^7.20.5",
|
||||||
|
@ -28,14 +28,22 @@
|
||||||
"typescript": "^5.3.2"
|
"typescript": "^5.3.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-syntax-decorators": "^7.23.3",
|
|
||||||
"@babel/core": "^7.23.3",
|
"@babel/core": "^7.23.3",
|
||||||
|
"@babel/generator": "^7.23.6",
|
||||||
|
"@babel/parser": "^7.24.4",
|
||||||
|
"@babel/plugin-syntax-decorators": "^7.23.3",
|
||||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
"@babel/plugin-syntax-jsx": "7.16.7",
|
||||||
"@babel/plugin-syntax-typescript": "^7.23.3",
|
"@babel/plugin-syntax-typescript": "^7.23.3",
|
||||||
|
"@babel/traverse": "^7.24.1",
|
||||||
|
"@babel/types": "^7.24.0",
|
||||||
|
"@openinula/class-transformer": "workspace:*",
|
||||||
"@openinula/reactivity-parser": "workspace:*",
|
"@openinula/reactivity-parser": "workspace:*",
|
||||||
"@openinula/view-generator": "workspace:*",
|
"@openinula/view-generator": "workspace:*",
|
||||||
"@openinula/view-parser": "workspace:*",
|
"@openinula/view-parser": "workspace:*",
|
||||||
"@openinula/class-transformer": "workspace:*",
|
"@types/babel-types": "^7.0.15",
|
||||||
|
"@types/babel__generator": "^7.6.8",
|
||||||
|
"@types/babel__parser": "^7.1.1",
|
||||||
|
"@types/babel__traverse": "^7.6.8",
|
||||||
"jsx-view-parser": "workspace:*",
|
"jsx-view-parser": "workspace:*",
|
||||||
"minimatch": "^9.0.3",
|
"minimatch": "^9.0.3",
|
||||||
"vitest": "^1.4.0"
|
"vitest": "^1.4.0"
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* 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, type types as t } from '@babel/core';
|
||||||
|
import { createComponentNode, createCondNode, createJSXNode } from './nodeFactory';
|
||||||
|
import { AnalyzeContext, Branch, Visitor } from './types';
|
||||||
|
import { isValidPath } from './utils';
|
||||||
|
|
||||||
|
export function earlyReturnAnalyze(): Visitor {
|
||||||
|
return {
|
||||||
|
ReturnStatement(path: NodePath<t.ReturnStatement>, context: AnalyzeContext) {
|
||||||
|
const currentComp = context.currentComponent;
|
||||||
|
|
||||||
|
const argument = path.get('argument');
|
||||||
|
if (argument.isJSXElement()) {
|
||||||
|
currentComp.child = createJSXNode(currentComp, argument);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IfStatement(ifStmt: NodePath<t.IfStatement>, context: AnalyzeContext) {
|
||||||
|
if (!hasEarlyReturn(ifStmt)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentComp = context.currentComponent;
|
||||||
|
|
||||||
|
const branches: Branch[] = [];
|
||||||
|
let next: NodePath<t.Statement> | null = ifStmt;
|
||||||
|
let branchIdx = 0;
|
||||||
|
|
||||||
|
// Walk through the if-else chain to create branches
|
||||||
|
while (next && next.isIfStatement()) {
|
||||||
|
const nextConditions = [next.get('test')];
|
||||||
|
// gen id for branch with babel
|
||||||
|
const name = `$$branch-${branchIdx}`;
|
||||||
|
branches.push({
|
||||||
|
conditions: nextConditions,
|
||||||
|
content: createComponentNode(name, getStatements(ifStmt.get('consequent')), currentComp),
|
||||||
|
});
|
||||||
|
|
||||||
|
const elseBranch: NodePath<t.Statement | null | undefined> = next.get('alternate');
|
||||||
|
next = isValidPath(elseBranch) ? elseBranch : null;
|
||||||
|
branchIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time for the else branch
|
||||||
|
// We merge the else branch with the rest statements in fc body to form the children
|
||||||
|
const elseBranch = next ? getStatements(next) : [];
|
||||||
|
const defaultComponent = createComponentNode(
|
||||||
|
'$$branch-default',
|
||||||
|
elseBranch.concat(context.restStmt),
|
||||||
|
currentComp
|
||||||
|
);
|
||||||
|
context.skipRest();
|
||||||
|
|
||||||
|
currentComp.child = createCondNode(currentComp, defaultComponent, branches);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatements(next: NodePath<t.Statement>) {
|
||||||
|
return next.isBlockStatement() ? next.get('body') : [next];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasEarlyReturn(path: NodePath<t.Node>) {
|
||||||
|
let hasReturn = false;
|
||||||
|
path.traverse({
|
||||||
|
ReturnStatement(path: NodePath<t.ReturnStatement>) {
|
||||||
|
if (
|
||||||
|
path.parentPath.isFunctionDeclaration() ||
|
||||||
|
path.parentPath.isFunctionExpression() ||
|
||||||
|
path.parentPath.isArrowFunctionExpression()
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasReturn = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return hasReturn;
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { NodePath, type types as t } from '@babel/core';
|
||||||
|
import { jsxSlicesAnalyze } from './jsxSliceAnalyze';
|
||||||
|
import { earlyReturnAnalyze } from './earlyReturnAnalyze';
|
||||||
|
import { AnalyzeContext, Analyzer, ComponentNode, CondNode, Visitor } from './types';
|
||||||
|
import { createComponentNode } from './nodeFactory';
|
||||||
|
import { propertiesAnalyze } from './propertiesAnalyze';
|
||||||
|
import { isValidComponent } from './utils';
|
||||||
|
|
||||||
|
const builtinAnalyzers = [jsxSlicesAnalyze, earlyReturnAnalyze, propertiesAnalyze];
|
||||||
|
let analyzers: Analyzer[] = builtinAnalyzers;
|
||||||
|
|
||||||
|
export function isCondNode(node: any): node is CondNode {
|
||||||
|
return node && node.type === 'cond';
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
||||||
|
return visitors.reduce((acc, cur) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
...cur(),
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk through the function component body
|
||||||
|
export function iterateFCBody(bodyStatements: NodePath<t.Statement>[], componentNode: ComponentNode, level = 0) {
|
||||||
|
const visitor = mergeVisitor(...analyzers);
|
||||||
|
const visit = (p: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (p.isReturnStatement()) {
|
||||||
|
visitor.ReturnStatement?.(p, context);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipRest) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The process of analyzing the component
|
||||||
|
* 1. identify the component
|
||||||
|
* 2. identify the jsx slice in the 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 path
|
||||||
|
* @param customAnalyzers
|
||||||
|
*/
|
||||||
|
export function analyze(path: NodePath<t.FunctionDeclaration>, customAnalyzers?: Analyzer[]) {
|
||||||
|
const node = path.node;
|
||||||
|
if (!isValidComponent(node)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (customAnalyzers) {
|
||||||
|
analyzers = customAnalyzers;
|
||||||
|
}
|
||||||
|
const fnName = node.id.name;
|
||||||
|
const root = createComponentNode(fnName, path.get('body').get('body'));
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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 { AnalyzeContext, Visitor } from './types';
|
||||||
|
import { createSubCompNode } from './nodeFactory';
|
||||||
|
import * as t from '@babel/types';
|
||||||
|
|
||||||
|
function genName(tagName: string, ctx: AnalyzeContext) {
|
||||||
|
return `$$${tagName}-Sub${ctx.currentComponent.subComponents.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function genNameFromJSX(path: NodePath<t.JSXElement>, ctx: AnalyzeContext) {
|
||||||
|
const tagId = path.get('openingElement').get('name');
|
||||||
|
if (tagId.isJSXIdentifier()) {
|
||||||
|
const jsxName = tagId.node.name;
|
||||||
|
return genName(jsxName, ctx);
|
||||||
|
}
|
||||||
|
throw new Error('JSXMemberExpression is not supported yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// replace with the subComp jsxElement
|
||||||
|
const subCompJSX = t.jsxElement(
|
||||||
|
t.jsxOpeningElement(t.jsxIdentifier(name), [], true),
|
||||||
|
t.jsxClosingElement(t.jsxIdentifier(name)),
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
path.replaceWith(subCompJSX);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze the JSX slice in the function component
|
||||||
|
* 1. VariableDeclaration, like `const a = <div />`
|
||||||
|
* 2. SubComponent, like `function Sub() { return <div /> }`
|
||||||
|
*
|
||||||
|
* i.e.
|
||||||
|
* ```jsx
|
||||||
|
* let jsxSlice = <div>{count}</div>
|
||||||
|
* // =>
|
||||||
|
* function Comp_$id$() {
|
||||||
|
* return <div>{count}</div>
|
||||||
|
* }
|
||||||
|
* let jsxSlice = <Comp_$id$/>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function jsxSlicesAnalyze(): Visitor {
|
||||||
|
return {
|
||||||
|
JSXElement(path: NodePath<t.JSXElement>, ctx) {
|
||||||
|
const name = genNameFromJSX(path, ctx);
|
||||||
|
replaceJSXSliceWithSubComp(name, ctx, path);
|
||||||
|
path.skip();
|
||||||
|
},
|
||||||
|
JSXFragment(path: NodePath<t.JSXFragment>, ctx) {
|
||||||
|
replaceJSXSliceWithSubComp('frag', ctx, path);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* 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, type types as t } from '@babel/core';
|
||||||
|
import { Branch, ComponentNode, CondNode, InulaNode, JSX, JSXNode, SubCompNode } from './types';
|
||||||
|
import { iterateFCBody } from './index';
|
||||||
|
|
||||||
|
export function createComponentNode(
|
||||||
|
name: string,
|
||||||
|
fnBody: NodePath<t.Statement>[],
|
||||||
|
parent?: ComponentNode
|
||||||
|
): ComponentNode {
|
||||||
|
const comp: ComponentNode = {
|
||||||
|
type: 'comp',
|
||||||
|
name,
|
||||||
|
props: {},
|
||||||
|
child: undefined,
|
||||||
|
subComponents: [],
|
||||||
|
methods: [],
|
||||||
|
state: [],
|
||||||
|
parent,
|
||||||
|
fnBody,
|
||||||
|
};
|
||||||
|
|
||||||
|
iterateFCBody(fnBody, comp);
|
||||||
|
|
||||||
|
return comp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addState(comp: ComponentNode, name: string, value: t.Expression | null) {
|
||||||
|
comp.state.push({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addMethod(comp: ComponentNode, method: NodePath<t.FunctionDeclaration>) {
|
||||||
|
comp.methods.push(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createJSXNode(parent: ComponentNode, content: NodePath<JSX>): JSXNode {
|
||||||
|
return {
|
||||||
|
type: 'jsx',
|
||||||
|
parent,
|
||||||
|
child: content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCondNode(parent: ComponentNode, child: InulaNode, branches: Branch[]): CondNode {
|
||||||
|
return {
|
||||||
|
type: 'cond',
|
||||||
|
branches,
|
||||||
|
child,
|
||||||
|
parent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSubCompNode(name: string, parent: ComponentNode, child: JSX): SubCompNode {
|
||||||
|
return {
|
||||||
|
type: 'subComp',
|
||||||
|
name,
|
||||||
|
parent,
|
||||||
|
child,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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 { 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) {
|
||||||
|
const declarations = path.get('declarations');
|
||||||
|
// iterate the declarations
|
||||||
|
declarations.forEach(declaration => {
|
||||||
|
const id = declaration.get('id');
|
||||||
|
// handle destructuring
|
||||||
|
if (id.isObjectPattern()) {
|
||||||
|
throw new Error('Object destructuring is not supported yet');
|
||||||
|
} else if (id.isArrayPattern()) {
|
||||||
|
// TODO: handle array destructuring
|
||||||
|
throw new Error('Array destructuring is not supported yet');
|
||||||
|
} else if (id.isIdentifier()) {
|
||||||
|
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>);
|
||||||
|
}
|
||||||
|
// handle jsx slice
|
||||||
|
ctx.traverse(path, ctx);
|
||||||
|
}
|
||||||
|
addState(ctx.currentComponent, id.node.name, declaration.node.init || null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
FunctionDeclaration(path: NodePath<t.FunctionDeclaration>, ctx) {
|
||||||
|
const fnId = path.node.id;
|
||||||
|
if (!fnId) {
|
||||||
|
// This is an anonymous function, collect into lifecycle
|
||||||
|
//TODO
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasJSX(path)) {
|
||||||
|
// This is a normal function, collect into methods
|
||||||
|
addMethod(ctx.currentComponent, path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFn(fnId.name, path.get('body'));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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, types as t } from '@babel/core';
|
||||||
|
import { Node } from '@babel/traverse';
|
||||||
|
|
||||||
|
// --- Node shape ---
|
||||||
|
export type InulaNode = ComponentNode | CondNode | JSXNode;
|
||||||
|
export type JSX = t.JSXElement | t.JSXFragment;
|
||||||
|
|
||||||
|
type defaultVal = any | null;
|
||||||
|
type Bitmap = number;
|
||||||
|
interface Reactive {
|
||||||
|
name: string;
|
||||||
|
value: t.Expression | null;
|
||||||
|
// indicate the value is a state or computed or watch
|
||||||
|
listeners: string[];
|
||||||
|
bitmap: Bitmap;
|
||||||
|
// need a flag for computed to gen a getter
|
||||||
|
// watch is a static computed
|
||||||
|
isComputed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>[];
|
||||||
|
child?: InulaNode;
|
||||||
|
subComponents: ComponentNode[];
|
||||||
|
parent?: ComponentNode;
|
||||||
|
/**
|
||||||
|
* The function body of the fn component code
|
||||||
|
*/
|
||||||
|
// fnBody: NodePath<t.Statement>[];
|
||||||
|
// a map to find the state
|
||||||
|
reactiveMap: Record<string, Bitmap>;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubCompNode {
|
||||||
|
type: 'subComp';
|
||||||
|
name: string;
|
||||||
|
parent: ComponentNode;
|
||||||
|
child: JSX;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JSXNode {
|
||||||
|
type: 'jsx';
|
||||||
|
parent: ComponentNode;
|
||||||
|
child: NodePath<JSX>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CondNode {
|
||||||
|
type: 'cond';
|
||||||
|
branches: Branch[];
|
||||||
|
parent: ComponentNode;
|
||||||
|
/**
|
||||||
|
* The default branch
|
||||||
|
*/
|
||||||
|
child: InulaNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Branch {
|
||||||
|
conditions: NodePath<t.Expression>[];
|
||||||
|
content: InulaNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalyzeContext {
|
||||||
|
level: number;
|
||||||
|
index: number;
|
||||||
|
currentComponent: ComponentNode;
|
||||||
|
restStmt: NodePath<t.Statement>[];
|
||||||
|
// --- flow control ---
|
||||||
|
/**
|
||||||
|
* ignore the rest of the statements
|
||||||
|
*/
|
||||||
|
skipRest: () => void;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
export type Analyzer = () => Visitor;
|
||||||
|
|
||||||
|
export interface FnComponentDeclaration extends t.FunctionDeclaration {
|
||||||
|
id: t.Identifier;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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, type types as t } from '@babel/core';
|
||||||
|
import { FnComponentDeclaration } from './types';
|
||||||
|
|
||||||
|
export function isValidPath<T>(path: NodePath<T>): path is NodePath<Exclude<T, undefined | null>> {
|
||||||
|
return !!path.node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The component name must be UpperCamelCase
|
||||||
|
export function isValidComponent(node: t.FunctionDeclaration): node is FnComponentDeclaration {
|
||||||
|
// the first letter of the component name must be uppercase
|
||||||
|
return node.id ? isValidComponentName(node.id.name) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidComponentName(name: string) {
|
||||||
|
// the first letter of the component name must be uppercase
|
||||||
|
return /^[A-Z]/.test(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasJSX(path: NodePath<t.Node>) {
|
||||||
|
if (path.isJSXElement()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there is JSXElement in the children
|
||||||
|
let seen = false;
|
||||||
|
path.traverse({
|
||||||
|
JSXElement() {
|
||||||
|
seen = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return seen;
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import dlight from './plugin';
|
||||||
import { type DLightOption } from './types';
|
import { type DLightOption } from './types';
|
||||||
import { type ConfigAPI, type TransformOptions } from '@babel/core';
|
import { type ConfigAPI, type TransformOptions } from '@babel/core';
|
||||||
import { plugin as fn2Class } from '@openinula/class-transformer';
|
import { plugin as fn2Class } from '@openinula/class-transformer';
|
||||||
|
import { parse as babelParse } from '@babel/parser';
|
||||||
|
|
||||||
export default function (_: ConfigAPI, options: DLightOption): TransformOptions {
|
export default function (_: ConfigAPI, options: DLightOption): TransformOptions {
|
||||||
return {
|
return {
|
||||||
|
@ -19,3 +20,18 @@ export default function (_: ConfigAPI, options: DLightOption): TransformOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type DLightOption };
|
export { type DLightOption };
|
||||||
|
|
||||||
|
export function parse(code: string) {
|
||||||
|
const result = babelParse(code, {
|
||||||
|
// parse in strict mode and allow module declarations
|
||||||
|
sourceType: 'module',
|
||||||
|
|
||||||
|
plugins: ['jsx'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.errors.length) {
|
||||||
|
throw new Error(result.errors[0].message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = result.program;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
|
const { types } = api;
|
||||||
|
const {
|
||||||
|
files = '**/*.{js,ts,jsx,tsx}',
|
||||||
|
excludeFiles = '**/{dist,node_modules,lib}/*',
|
||||||
|
enableDevTools = false,
|
||||||
|
htmlTags = defaultHtmlTags => defaultHtmlTags,
|
||||||
|
attributeMap = defaultAttributeMap,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const pluginProvider = new PluginProviderClass(
|
||||||
|
api,
|
||||||
|
types,
|
||||||
|
Array.isArray(files) ? files : [files],
|
||||||
|
Array.isArray(excludeFiles) ? excludeFiles : [excludeFiles],
|
||||||
|
enableDevTools,
|
||||||
|
htmlTags,
|
||||||
|
attributeMap
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
Program: {
|
||||||
|
enter(path, { filename }) {
|
||||||
|
return pluginProvider.programEnterVisitor(path, filename);
|
||||||
|
},
|
||||||
|
exit: pluginProvider.programExitVisitor.bind(pluginProvider),
|
||||||
|
},
|
||||||
|
FunctionDeclaration: {
|
||||||
|
enter: path => {
|
||||||
|
analyze(path);
|
||||||
|
},
|
||||||
|
exit: pluginProvider.classExit.bind(pluginProvider),
|
||||||
|
},
|
||||||
|
ClassMethod: pluginProvider.classMethodVisitor.bind(pluginProvider),
|
||||||
|
ClassProperty: pluginProvider.classPropertyVisitor.bind(pluginProvider),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -27,6 +27,45 @@ describe('condition', () => {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
).toMatchInlineSnapshot();
|
).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 "@openinula/next";
|
||||||
|
class App extends View {
|
||||||
|
Body() {
|
||||||
|
let $node0, $node1;
|
||||||
|
this._$update = $changed => {
|
||||||
|
$node1 && $node1.update($changed);
|
||||||
|
};
|
||||||
|
$node0 = $$createElement("div");
|
||||||
|
$node1 = new $$CondNode(0, $thisCond => {
|
||||||
|
if (count > 1) {
|
||||||
|
if ($thisCond.cond === 0) {
|
||||||
|
$thisCond.didntChange = true;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$thisCond.cond = 0;
|
||||||
|
let $node0, $node1;
|
||||||
|
$thisCond.updateFunc = $changed => {};
|
||||||
|
$node0 = new $$ExpNode(count, []);
|
||||||
|
$node1 = $$createTextNode(" is bigger than is 1", []);
|
||||||
|
return $thisCond.cond === 0 ? [$node0, $node1] : $thisCond.updateCond();
|
||||||
|
} else {
|
||||||
|
if ($thisCond.cond === 1) {
|
||||||
|
$thisCond.didntChange = true;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$thisCond.cond = 1;
|
||||||
|
let $node0, $node1;
|
||||||
|
$thisCond.updateFunc = $changed => {};
|
||||||
|
$node0 = new $$ExpNode(count, []);
|
||||||
|
$node1 = $$createTextNode(" is smaller than 1", []);
|
||||||
|
return $thisCond.cond === 1 ? [$node0, $node1] : $thisCond.updateCond();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$$insertNode($node0, $node1, 0);
|
||||||
|
$node0._$nodes = [$node1];
|
||||||
|
return [$node0];
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* 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 { isCondNode } from '../src/analyze';
|
||||||
|
import { mockAnalyze } from './mock';
|
||||||
|
|
||||||
|
describe('analyze early return', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
if (count > 1) {
|
||||||
|
return <div>1</div>
|
||||||
|
}
|
||||||
|
return <div>
|
||||||
|
<if cond={count > 1}>{count} is bigger than is 1</if>
|
||||||
|
<else>{count} is smaller than 1</else>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const branchNode = root?.child;
|
||||||
|
if (!isCondNode(branchNode)) {
|
||||||
|
throw new Error('Should be branch node');
|
||||||
|
}
|
||||||
|
expect(branchNode.branches.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with multi if', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
if (count > 1) {
|
||||||
|
return <div>1</div>
|
||||||
|
}
|
||||||
|
if (count > 2) {
|
||||||
|
return <div>2</div>
|
||||||
|
}
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const branchNode = root?.child;
|
||||||
|
if (!isCondNode(branchNode)) {
|
||||||
|
throw new Error('Should be branch node');
|
||||||
|
}
|
||||||
|
expect(branchNode.branches.length).toBe(1);
|
||||||
|
const subBranch = branchNode.child.child;
|
||||||
|
if (!isCondNode(subBranch)) {
|
||||||
|
throw new Error('SubBranchNode should be branch node');
|
||||||
|
}
|
||||||
|
expect(subBranch.branches.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with nested if', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
if (count > 1) {
|
||||||
|
if (count > 2) {
|
||||||
|
return <div>2</div>
|
||||||
|
}
|
||||||
|
return <div>1</div>
|
||||||
|
}
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const branchNode = root?.child;
|
||||||
|
if (!isCondNode(branchNode)) {
|
||||||
|
throw new Error('Should be branch node');
|
||||||
|
}
|
||||||
|
expect(branchNode.branches.length).toBe(1);
|
||||||
|
const subBranchNode = branchNode.branches[0].content.child;
|
||||||
|
if (!isCondNode(subBranchNode)) {
|
||||||
|
throw new Error('SubBranchNode should be branch node');
|
||||||
|
}
|
||||||
|
expect(subBranchNode.branches.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* 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 { 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';
|
||||||
|
|
||||||
|
export function mockAnalyze(code: string): ComponentNode {
|
||||||
|
let root: ComponentNode | null = null;
|
||||||
|
transformWithBabel(code, {
|
||||||
|
plugins: [
|
||||||
|
syntaxJSX.default ?? syntaxJSX,
|
||||||
|
function (api: typeof babel): PluginObj {
|
||||||
|
const { types } = api;
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
FunctionDeclaration: {
|
||||||
|
enter: path => {
|
||||||
|
root = analyze(path);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
|
filename: 'test.tsx',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
throw new Error('root is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genCode(ast: t.Node | null) {
|
||||||
|
if (!ast) {
|
||||||
|
throw new Error('ast is null');
|
||||||
|
}
|
||||||
|
return generate(ast).code;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printTree(node: InulaNode | undefined): any {
|
||||||
|
if (!node) {
|
||||||
|
return 'empty';
|
||||||
|
}
|
||||||
|
if (node.type === 'cond') {
|
||||||
|
return {
|
||||||
|
type: node.type,
|
||||||
|
branch: node.branches.map(b => printTree(b.content)),
|
||||||
|
children: printTree(node.child),
|
||||||
|
};
|
||||||
|
} else if (node.type === 'comp') {
|
||||||
|
return {
|
||||||
|
type: node.type,
|
||||||
|
children: printTree(node.child),
|
||||||
|
};
|
||||||
|
} else if (node.type === 'jsx') {
|
||||||
|
return {
|
||||||
|
type: node.type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* 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 generate from '@babel/generator';
|
||||||
|
|
||||||
|
describe('propertiesAnalyze', () => {
|
||||||
|
describe('state', () => {
|
||||||
|
it('should work with jsx slice', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
const a = <div></div>
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(root.state[0].name).toBe('a');
|
||||||
|
expect(genCode(root.state[0].value)).toMatchInlineSnapshot(`"<$$div-Sub0 />"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with jsx slice in ternary operator', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
const a = true ? <div></div> : <h1></h1>
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(root.state[0].name).toBe('a');
|
||||||
|
expect(root.subComponents[0].name).toBe('$$div-Sub0');
|
||||||
|
expect(genCode(root.subComponents[0].child)).toMatchInlineSnapshot(`"<div></div>"`);
|
||||||
|
expect(root.subComponents[1].name).toBe('$$h1-Sub1');
|
||||||
|
expect(genCode(root.subComponents[1].child)).toMatchInlineSnapshot(`"<h1></h1>"`);
|
||||||
|
expect(genCode(root.state[0].value)).toMatchInlineSnapshot(`"true ? <$$div-Sub0 /> : <$$h1-Sub1 />"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with jsx slice in arr', () => {
|
||||||
|
const root = mockAnalyze(`
|
||||||
|
function App() {
|
||||||
|
const arr = [<div></div>,<h1></h1>]
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(root.state[0].name).toBe('a');
|
||||||
|
expect(root.subComponents[0].name).toBe('$$div-Sub0');
|
||||||
|
expect(genCode(root.subComponents[0].child)).toMatchInlineSnapshot(`"<div></div>"`);
|
||||||
|
expect(root.subComponents[1].name).toBe('$$h1-Sub1');
|
||||||
|
expect(genCode(root.subComponents[1].child)).toMatchInlineSnapshot(`"<h1></h1>"`);
|
||||||
|
expect(genCode(root.state[0].value)).toMatchInlineSnapshot(`"true ? <$$div-Sub0 /> : <$$h1-Sub1 />"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -13,7 +13,7 @@
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.cjs",
|
"main": "src/index.ts",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"typings": "dist/index.d.ts",
|
"typings": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
Loading…
Reference in New Issue