feat: autoNaming, autoReturn, deconstructing, jsxSlice and props
This commit is contained in:
parent
4ca2d66fac
commit
dabc0eed20
|
@ -18,7 +18,7 @@ import { LifeCycle, Visitor } from './types';
|
||||||
import { addLifecycle, addWatch } from './nodeFactory';
|
import { addLifecycle, addWatch } from './nodeFactory';
|
||||||
import { types as t } from '@openinula/babel-api';
|
import { types as t } from '@openinula/babel-api';
|
||||||
import { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
import { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
||||||
import { extractFnFromMacro, getFnBody } from '../utils';
|
import { extractFnFromMacro, getFnBodyPath } from '../utils';
|
||||||
import { getDependenciesFromNode } from './reactive/getDependencies';
|
import { getDependenciesFromNode } from './reactive/getDependencies';
|
||||||
|
|
||||||
function isLifeCycleName(name: string): name is LifeCycle {
|
function isLifeCycleName(name: string): name is LifeCycle {
|
||||||
|
@ -44,7 +44,7 @@ export function functionalMacroAnalyze(): Visitor {
|
||||||
// lifecycle
|
// lifecycle
|
||||||
if (isLifeCycleName(calleeName)) {
|
if (isLifeCycleName(calleeName)) {
|
||||||
const fnNode = extractFnFromMacro(expression, calleeName);
|
const fnNode = extractFnFromMacro(expression, calleeName);
|
||||||
addLifecycle(ctx.current, calleeName, getFnBody(fnNode).node);
|
addLifecycle(ctx.current, calleeName, getFnBodyPath(fnNode).node);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { type NodePath } from '@babel/core';
|
import { type NodePath } from '@babel/core';
|
||||||
import { propsAnalyze } from './propsAnalyze';
|
|
||||||
import { AnalyzeContext, Analyzer, ComponentNode, Visitor } from './types';
|
import { AnalyzeContext, Analyzer, ComponentNode, Visitor } from './types';
|
||||||
import { addLifecycle, createComponentNode } from './nodeFactory';
|
import { addLifecycle, createComponentNode } from './nodeFactory';
|
||||||
import { variablesAnalyze } from './variablesAnalyze';
|
import { variablesAnalyze } from './variablesAnalyze';
|
||||||
import { functionalMacroAnalyze } from './functionalMacroAnalyze';
|
import { functionalMacroAnalyze } from './functionalMacroAnalyze';
|
||||||
import { getFnBody } from '../utils';
|
import { getFnBodyPath } from '../utils';
|
||||||
import { viewAnalyze } from './viewAnalyze';
|
import { viewAnalyze } from './viewAnalyze';
|
||||||
import { WILL_MOUNT } from '../constants';
|
import { WILL_MOUNT } from '../constants';
|
||||||
import { types as t } from '@openinula/babel-api';
|
import { types as t } from '@openinula/babel-api';
|
||||||
const builtinAnalyzers = [propsAnalyze, variablesAnalyze, functionalMacroAnalyze, viewAnalyze];
|
const builtinAnalyzers = [variablesAnalyze, functionalMacroAnalyze, viewAnalyze];
|
||||||
|
|
||||||
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
||||||
return visitors.reduce<Visitor<AnalyzeContext>>((acc, cur) => {
|
return visitors.reduce<Visitor<AnalyzeContext>>((acc, cur) => {
|
||||||
|
@ -50,7 +49,7 @@ export function analyzeFnComp(
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- analyze the function body ---
|
// --- analyze the function body ---
|
||||||
const bodyStatements = getFnBody(fnNode).get('body');
|
const bodyStatements = getFnBodyPath(fnNode).get('body');
|
||||||
for (let i = 0; i < bodyStatements.length; i++) {
|
for (let i = 0; i < bodyStatements.length; i++) {
|
||||||
const p = bodyStatements[i];
|
const p = bodyStatements[i];
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,10 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
import { types as t } from '@openinula/babel-api';
|
||||||
|
import { NodePath } from '@babel/core';
|
||||||
import { FnComponentDeclaration } from './types';
|
import { FnComponentDeclaration } from './types';
|
||||||
|
import { ArrowFunctionWithBlock } from '../utils';
|
||||||
|
|
||||||
export function isValidPath<T>(path: NodePath<T>): path is NodePath<Exclude<T, undefined | null>> {
|
export function isValidPath<T>(path: NodePath<T>): path is NodePath<Exclude<T, undefined | null>> {
|
||||||
return !!path.node;
|
return !!path.node;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export const COMPONENT = 'Component';
|
export const COMPONENT = 'Component';
|
||||||
|
export const Hook = 'Hook';
|
||||||
export const WILL_MOUNT = 'willMount';
|
export const WILL_MOUNT = 'willMount';
|
||||||
export const ON_MOUNT = 'onMount';
|
export const ON_MOUNT = 'onMount';
|
||||||
export const WILL_UNMOUNT = 'willUnmount';
|
export const WILL_UNMOUNT = 'willUnmount';
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import type babel from '@babel/core';
|
import type babel from '@babel/core';
|
||||||
import { type PluginObj } from '@babel/core';
|
import { NodePath, type PluginObj, type types as t } from '@babel/core';
|
||||||
import { type DLightOption } from './types';
|
import { type DLightOption } from './types';
|
||||||
import { defaultAttributeMap, defaultHTMLTags } from './const';
|
import { defaultAttributeMap, defaultHTMLTags } from './const';
|
||||||
import { analyze } from './analyzer';
|
import { analyze } from './analyzer';
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
|
||||||
import { COMPONENT } from './constants';
|
import { COMPONENT } from './constants';
|
||||||
import { extractFnFromMacro } from './utils';
|
import { extractFnFromMacro, isCompPath } from './utils';
|
||||||
import { register } from '@openinula/babel-api';
|
import { register } from '@openinula/babel-api';
|
||||||
|
|
||||||
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
|
@ -37,10 +36,7 @@ export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CallExpression(path: NodePath<t.CallExpression>) {
|
CallExpression(path: NodePath<t.CallExpression>) {
|
||||||
// find the component, like: Component(() => {})
|
if (isCompPath(path)) {
|
||||||
const callee = path.get('callee');
|
|
||||||
|
|
||||||
if (callee.isIdentifier() && callee.node.name === COMPONENT) {
|
|
||||||
const componentNode = extractFnFromMacro(path, COMPONENT);
|
const componentNode = extractFnFromMacro(path, COMPONENT);
|
||||||
let name = '';
|
let name = '';
|
||||||
// try to get the component name, when parent is a variable declarator
|
// try to get the component name, when parent is a variable declarator
|
||||||
|
|
|
@ -1 +1,68 @@
|
||||||
// Auto Naming for Component and Hook
|
import babel, { NodePath, PluginObj } from '@babel/core';
|
||||||
|
import { register, types as t } from '@openinula/babel-api';
|
||||||
|
import { isFnExp, createMacroNode, getFnBodyNode } from '../utils';
|
||||||
|
import { COMPONENT, Hook } from '../constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto Naming for Component and Hook
|
||||||
|
* Find the CamelCase name and transform it into Component marco
|
||||||
|
* function MyComponent() {} -> const MyComponent = Component(() => {})
|
||||||
|
* const MyComponent = () => {} -> const MyComponent = Component(() => {})
|
||||||
|
* const MyComponent = function() {} -> const MyComponent = Component(() => {})
|
||||||
|
*
|
||||||
|
* @param api
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
export default function (api: typeof babel): PluginObj {
|
||||||
|
register(api);
|
||||||
|
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
FunctionDeclaration(path: NodePath<t.FunctionDeclaration>) {
|
||||||
|
const { id } = path.node;
|
||||||
|
const macroNode = getMacroNode(id, path.node.body);
|
||||||
|
if (macroNode) {
|
||||||
|
path.replaceWith(macroNode);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
VariableDeclaration(path: NodePath<t.VariableDeclaration>) {
|
||||||
|
if (path.node.declarations.length === 1) {
|
||||||
|
const { id, init } = path.node.declarations[0];
|
||||||
|
if (t.isIdentifier(id) && isFnExp(init)) {
|
||||||
|
const macroNode = getMacroNode(id, getFnBodyNode(init));
|
||||||
|
|
||||||
|
if (macroNode) {
|
||||||
|
path.replaceWith(macroNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMacroNode(id: babel.types.Identifier | null | undefined, body: t.BlockStatement) {
|
||||||
|
const macroName = getMacroName(id?.name);
|
||||||
|
if (macroName) {
|
||||||
|
return t.variableDeclaration('const', [t.variableDeclarator(id!, createMacroNode(body, macroName))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMacroName(name: string | undefined) {
|
||||||
|
if (!name) return null;
|
||||||
|
|
||||||
|
if (isUpperCamelCase(name)) {
|
||||||
|
return COMPONENT;
|
||||||
|
} else if (isHook(name)) {
|
||||||
|
return Hook;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function isUpperCamelCase(str: string) {
|
||||||
|
return /^[A-Z]/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHook(str: string) {
|
||||||
|
return /^use[A-Z]/.test(str);
|
||||||
|
}
|
||||||
|
|
|
@ -13,63 +13,127 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
import babel, { NodePath, PluginObj } from '@babel/core';
|
||||||
import { createComponentNode, createCondNode, createJSXNode } from '../analyze/nodeFactory';
|
import { register, types as t } from '@openinula/babel-api';
|
||||||
import { AnalyzeContext, Branch, Visitor } from '../analyze/types';
|
import { isValidPath } from '../utils';
|
||||||
import { isValidPath } from '../analyze/utils';
|
import { extractFnFromMacro, isCompPath, getFnBodyPath, createMacroNode } from '../utils';
|
||||||
|
import { COMPONENT } from '../constants';
|
||||||
|
import { Scope } from '@babel/traverse';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a conditional node with branches
|
||||||
|
* ```jsx
|
||||||
|
* <if cond={count === 100}>
|
||||||
|
* <Comp_jf91a2 />
|
||||||
|
* </if>
|
||||||
|
* <else>
|
||||||
|
* <Comp_ao528j />
|
||||||
|
* </else>
|
||||||
|
* ```
|
||||||
|
* @param branches
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function generateCondNode(branches: Branch[]) {
|
||||||
|
const branchNodes = branches.map((branch, idx) => {
|
||||||
|
const tag = idx === 0 ? 'if' : idx === branches.length - 1 ? 'else' : 'elseif';
|
||||||
|
const conditionAttr = branch.conditions
|
||||||
|
? [t.jSXAttribute(t.jSXIdentifier('cond'), t.jsxExpressionContainer(branch.conditions))]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// The branch node is a jsx element, like <if cond={count === 100}><Comp_jf91a2 /></if>
|
||||||
|
return t.jsxElement(
|
||||||
|
t.jsxOpeningElement(t.jSXIdentifier(tag), conditionAttr),
|
||||||
|
t.jsxClosingElement(t.jSXIdentifier(tag)),
|
||||||
|
[t.jsxElement(t.jsxOpeningElement(t.jSXIdentifier(branch.name), [], true), null, [], true)]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return createFragmentNode(branchNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFragmentNode(children: t.JSXElement[]) {
|
||||||
|
return t.jsxElement(t.jsxOpeningElement(t.jSXIdentifier(''), []), t.jsxClosingElement(t.jSXIdentifier('')), children);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (api: typeof babel): PluginObj {
|
||||||
|
register(api);
|
||||||
|
|
||||||
export function earlyReturnPlugin(): Visitor {
|
|
||||||
return {
|
return {
|
||||||
ReturnStatement(path: NodePath<t.ReturnStatement>, context: AnalyzeContext) {
|
visitor: {
|
||||||
const currentComp = context.current;
|
CallExpression(path: NodePath<t.CallExpression>) {
|
||||||
|
if (isCompPath(path)) {
|
||||||
|
const fnPath = extractFnFromMacro(path, COMPONENT);
|
||||||
|
const bodyPath = getFnBodyPath(fnPath);
|
||||||
|
// iterate through the function body to find early return
|
||||||
|
const ifStmtIndex = bodyPath.get('body').findIndex(stmt => stmt.isIfStatement() && hasEarlyReturn(stmt));
|
||||||
|
if (ifStmtIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const argument = path.get('argument');
|
const branches = parseBranches(
|
||||||
if (argument.isJSXElement()) {
|
bodyPath.get('body')[ifStmtIndex] as NodePath<t.IfStatement>,
|
||||||
currentComp.children = createJSXNode(currentComp, argument);
|
bodyPath.node.body.slice(ifStmtIndex + 1)
|
||||||
}
|
);
|
||||||
},
|
|
||||||
IfStatement(ifStmt: NodePath<t.IfStatement>, context: AnalyzeContext) {
|
|
||||||
if (!hasEarlyReturn(ifStmt)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentComp = context.current;
|
|
||||||
|
|
||||||
const branches: Branch[] = [];
|
// At first, we remove the node after the if statement in the function body
|
||||||
let next: NodePath<t.Statement> | null = ifStmt;
|
let i = bodyPath.node.body.length - 1;
|
||||||
let branchIdx = 0;
|
while (i >= ifStmtIndex) {
|
||||||
|
bodyPath.get('body')[i].remove();
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
|
||||||
// Walk through the if-else chain to create branches
|
// Then we generate the every brach component
|
||||||
while (next && next.isIfStatement()) {
|
const branchNodes = branches.map(branch =>
|
||||||
const nextConditions = [next.get('test')];
|
t.variableDeclaration('const', [t.variableDeclarator(t.identifier(branch.name), branch.content)])
|
||||||
// gen id for branch with babel
|
);
|
||||||
const name = `$$branch-${branchIdx}`;
|
// push the branch components to the function body
|
||||||
branches.push({
|
bodyPath.pushContainer('body', branchNodes);
|
||||||
conditions: nextConditions,
|
|
||||||
content: createComponentNode(name, getStatements(ifStmt.get('consequent')), currentComp),
|
|
||||||
});
|
|
||||||
|
|
||||||
const elseBranch: NodePath<t.Statement | null | undefined> = next.get('alternate');
|
// At last, we generate the cond node
|
||||||
next = isValidPath(elseBranch) ? elseBranch : null;
|
const condNode = generateCondNode(branches);
|
||||||
branchIdx++;
|
bodyPath.pushContainer('body', t.returnStatement(condNode));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// 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.children = createCondNode(currentComp, defaultComponent, branches);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Branch {
|
||||||
|
name: string;
|
||||||
|
conditions?: t.Expression;
|
||||||
|
content: t.CallExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBranches(ifStmt: NodePath<t.IfStatement>, restStmt: t.Statement[]) {
|
||||||
|
const branches: Branch[] = [];
|
||||||
|
let next: NodePath<t.Statement> | null = ifStmt;
|
||||||
|
|
||||||
|
// Walk through the if-else chain to create branches
|
||||||
|
while (next && next.isIfStatement()) {
|
||||||
|
const nextConditions = next.node.test;
|
||||||
|
// gen id for branch with babel
|
||||||
|
branches.push({
|
||||||
|
name: genUid(ifStmt.scope, 'Branch'),
|
||||||
|
conditions: nextConditions,
|
||||||
|
content: createMacroNode(t.blockStatement(getStatements(ifStmt.get('consequent'))), COMPONENT),
|
||||||
|
});
|
||||||
|
|
||||||
|
const elseBranch: NodePath<t.Statement | null | undefined> = next.get('alternate');
|
||||||
|
next = isValidPath(elseBranch) ? elseBranch : null;
|
||||||
|
}
|
||||||
|
// Time for the else branch
|
||||||
|
// We merge the else branch with the rest statements in fc body to form the children
|
||||||
|
const elseBranch = next ? (next.isBlockStatement() ? next.node.body : [next.node]) : [];
|
||||||
|
branches.push({
|
||||||
|
name: genUid(ifStmt.scope, 'Default'),
|
||||||
|
content: createMacroNode(t.blockStatement(elseBranch.concat(restStmt)), COMPONENT),
|
||||||
|
});
|
||||||
|
|
||||||
|
return branches;
|
||||||
|
}
|
||||||
|
|
||||||
function getStatements(next: NodePath<t.Statement>) {
|
function getStatements(next: NodePath<t.Statement>) {
|
||||||
return next.isBlockStatement() ? next.get('body') : [next];
|
return next.isBlockStatement() ? next.node.body : [next.node];
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasEarlyReturn(path: NodePath<t.Node>) {
|
function hasEarlyReturn(path: NodePath<t.Node>) {
|
||||||
|
@ -88,3 +152,23 @@ function hasEarlyReturn(path: NodePath<t.Node>) {
|
||||||
});
|
});
|
||||||
return hasReturn;
|
return hasReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function genUid(scope: Scope, name: string) {
|
||||||
|
let result = name;
|
||||||
|
let i = 1;
|
||||||
|
do {
|
||||||
|
result = `${name}_${i}`;
|
||||||
|
i++;
|
||||||
|
} while (
|
||||||
|
scope.hasBinding(result) ||
|
||||||
|
scope.hasGlobal(result) ||
|
||||||
|
scope.hasReference(result) ||
|
||||||
|
scope.hasGlobal(result)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mark the id as a reference to prevent it from being renamed
|
||||||
|
const program = scope.getProgramParent();
|
||||||
|
program.references[result] = true;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
@ -13,37 +13,81 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NodePath } from '@babel/core';
|
import babel, { NodePath, PluginObj } from '@babel/core';
|
||||||
import { AnalyzeContext, Visitor } from '../analyze/types';
|
|
||||||
import { createSubCompNode } from '../analyze/nodeFactory';
|
|
||||||
import * as t from '@babel/types';
|
import * as t from '@babel/types';
|
||||||
|
import type { DLightOption } from '../types';
|
||||||
|
import { register } from '@openinula/babel-api';
|
||||||
|
|
||||||
function genName(tagName: string, ctx: AnalyzeContext) {
|
function transformJSXSlice(path: NodePath<t.JSXElement> | NodePath<t.JSXFragment>) {
|
||||||
return `$$${tagName}-Sub${ctx.current.subComponents.length}`;
|
path.skip();
|
||||||
}
|
|
||||||
|
|
||||||
function genNameFromJSX(path: NodePath<t.JSXElement>, ctx: AnalyzeContext) {
|
// don't handle the jsx in return statement or in the arrow function return
|
||||||
const tagId = path.get('openingElement').get('name');
|
if (path.parentPath.isReturnStatement() || path.parentPath.isArrowFunctionExpression()) {
|
||||||
if (tagId.isJSXIdentifier()) {
|
// skip the children
|
||||||
const jsxName = tagId.node.name;
|
return;
|
||||||
return genName(jsxName, ctx);
|
}
|
||||||
|
|
||||||
|
const sliceCompNode = t.callExpression(t.identifier('Component'), [t.arrowFunctionExpression([], path.node)]);
|
||||||
|
// handle the jsx in assignment, like `const a = <div></div>`
|
||||||
|
// transform it into:
|
||||||
|
// ```jsx
|
||||||
|
// const a = Component(() => {
|
||||||
|
// return <div></div>
|
||||||
|
// })
|
||||||
|
// ```
|
||||||
|
if (path.parentPath.isVariableDeclarator()) {
|
||||||
|
path.replaceWith(sliceCompNode);
|
||||||
|
} else {
|
||||||
|
// extract the jsx slice into a subcomponent, like const a = type? <div></div> : <span></span>
|
||||||
|
// transform it into:
|
||||||
|
// ```jsx
|
||||||
|
// const Div$$ = (() => {
|
||||||
|
// return <div></div>
|
||||||
|
// });
|
||||||
|
// const Span$$ = Component(() => {
|
||||||
|
// return <span></span>
|
||||||
|
// });
|
||||||
|
// const a = type? <Div$$/> : <Span$$/>;
|
||||||
|
// ```
|
||||||
|
const sliceId = path.scope.generateUidIdentifier(genName(path.node));
|
||||||
|
sliceId.name = 'JSX' + sliceId.name;
|
||||||
|
|
||||||
|
// insert the subcomponent
|
||||||
|
const sliceComp = t.variableDeclaration('const', [t.variableDeclarator(sliceId, sliceCompNode)]);
|
||||||
|
// insert into the previous statement
|
||||||
|
const stmt = path.getStatementParent();
|
||||||
|
if (!stmt) {
|
||||||
|
throw new Error('Cannot find the statement parent');
|
||||||
|
}
|
||||||
|
stmt.insertBefore(sliceComp);
|
||||||
|
// replace the jsx slice with the subcomponent
|
||||||
|
const sliceJsxId = t.jSXIdentifier(sliceId.name);
|
||||||
|
path.replaceWith(t.jsxElement(t.jsxOpeningElement(sliceJsxId, [], true), null, [], true));
|
||||||
}
|
}
|
||||||
throw new Error('JSXMemberExpression is not supported yet');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceJSXSliceWithSubComp(name: string, ctx: AnalyzeContext, path: NodePath<t.JSXElement | t.JSXFragment>) {
|
function genName(node: t.JSXElement | t.JSXFragment) {
|
||||||
// create a subComponent node and add it to the current component
|
if (t.isJSXFragment(node)) {
|
||||||
const subComp = createSubCompNode(name, ctx.current, path.node);
|
return 'Fragment';
|
||||||
ctx.current.subComponents.push(subComp);
|
}
|
||||||
|
|
||||||
// replace with the subComp jsxElement
|
const jsxName = node.openingElement.name;
|
||||||
const subCompJSX = t.jsxElement(
|
if (t.isJSXIdentifier(jsxName)) {
|
||||||
t.jsxOpeningElement(t.jsxIdentifier(name), [], true),
|
return jsxName.name;
|
||||||
t.jsxClosingElement(t.jsxIdentifier(name)),
|
} else if (t.isJSXMemberExpression(jsxName)) {
|
||||||
[],
|
// connect all parts with _
|
||||||
true
|
let result = jsxName.property.name;
|
||||||
);
|
let current: t.JSXMemberExpression | t.JSXIdentifier = jsxName.object;
|
||||||
path.replaceWith(subCompJSX);
|
while (t.isJSXMemberExpression(current)) {
|
||||||
|
result = current.property.name + '_' + result;
|
||||||
|
current = current.object;
|
||||||
|
}
|
||||||
|
result = current.name + '_' + result;
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
// JSXNamespacedName
|
||||||
|
return jsxName.name.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,40 +105,16 @@ function replaceJSXSliceWithSubComp(name: string, ctx: AnalyzeContext, path: Nod
|
||||||
* let jsxSlice = <Comp_$id$/>
|
* let jsxSlice = <Comp_$id$/>
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function jsxSlicesAnalyze(): Visitor {
|
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
|
register(api);
|
||||||
return {
|
return {
|
||||||
JSXElement(path: NodePath<t.JSXElement>, ctx) {
|
visitor: {
|
||||||
const name = genNameFromJSX(path, ctx);
|
JSXElement(path: NodePath<t.JSXElement>) {
|
||||||
replaceJSXSliceWithSubComp(name, ctx, path);
|
transformJSXSlice(path);
|
||||||
path.skip();
|
},
|
||||||
},
|
JSXFragment(path: NodePath<t.JSXFragment>) {
|
||||||
JSXFragment(path: NodePath<t.JSXFragment>, ctx) {
|
transformJSXSlice(path);
|
||||||
replaceJSXSliceWithSubComp('frag', ctx, path);
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Analyze the JSX slice in the function component, including:
|
|
||||||
// 1. VariableDeclaration, like `const a = <div />`
|
|
||||||
// 2. SubComponent, like `function Sub() { return <div /> }`
|
|
||||||
function handleFn(fnName: string, fnBody: NodePath<types.BlockStatement>) {
|
|
||||||
if (isValidComponentName(fnName)) {
|
|
||||||
// This is a subcomponent, treat it as a normal component
|
|
||||||
} else {
|
|
||||||
// This is jsx creation function
|
|
||||||
// function jsxFunc() {
|
|
||||||
// // This is a function that returns JSX
|
|
||||||
// // because the function name is smallCamelCased
|
|
||||||
// return <div>{count}</div>
|
|
||||||
// }
|
|
||||||
// =>
|
|
||||||
// function jsxFunc() {
|
|
||||||
// function Comp_$id4$() {
|
|
||||||
// return <div>{count}</div>
|
|
||||||
// }
|
|
||||||
// // This is a function that returns JSX
|
|
||||||
// // because the function name is smallCamelCased
|
|
||||||
// return <Comp_$id4$/>
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
import babel, { NodePath, PluginObj } from '@babel/core';
|
||||||
|
import type { DLightOption } from '../types';
|
||||||
|
import { register } from '@openinula/babel-api';
|
||||||
|
import { COMPONENT } from '../constants';
|
||||||
|
import { ArrowFunctionWithBlock, extractFnFromMacro, isCompPath, wrapArrowFunctionWithBlock } from '../utils';
|
||||||
|
import { types as t } from '@openinula/babel-api';
|
||||||
|
|
||||||
|
export enum PropType {
|
||||||
|
REST = 'rest',
|
||||||
|
SINGLE = 'single',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Prop {
|
||||||
|
name: string;
|
||||||
|
type: PropType;
|
||||||
|
alias?: string | null;
|
||||||
|
defaultVal?: t.Expression | null;
|
||||||
|
nestedProps?: string[] | null;
|
||||||
|
nestedRelationship?: t.ObjectPattern | t.ArrayPattern | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. function({ prop1, prop2: [p20, p21] }) {}
|
||||||
|
// transform into
|
||||||
|
// function(prop1, prop2: [p20, p21]) {
|
||||||
|
// let prop1_$$prop
|
||||||
|
// let prop2_$$prop
|
||||||
|
// let p20
|
||||||
|
// let p21
|
||||||
|
// }
|
||||||
|
function createPropAssignment(prop: Prop) {
|
||||||
|
const decalrations = [
|
||||||
|
t.variableDeclaration('let', [t.variableDeclarator(t.identifier(`${prop.name}_$$prop`), prop.defaultVal)]),
|
||||||
|
];
|
||||||
|
if (prop.alias) {
|
||||||
|
decalrations.push(
|
||||||
|
t.variableDeclaration('let', [
|
||||||
|
t.variableDeclarator(t.identifier(`${prop.alias}`), t.identifier(`${prop.name}_$$prop`)),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (prop.nestedRelationship) {
|
||||||
|
decalrations.push(
|
||||||
|
t.variableDeclaration('let', [t.variableDeclarator(prop.nestedRelationship, t.identifier(`${prop.name}_$$prop`))])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return decalrations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPropsDestructing(
|
||||||
|
fnPath: NodePath<t.FunctionExpression> | NodePath<ArrowFunctionWithBlock>,
|
||||||
|
propsName: string
|
||||||
|
) {
|
||||||
|
let props: Prop[] = [];
|
||||||
|
const body = fnPath.get('body') as NodePath<t.BlockStatement>;
|
||||||
|
body.traverse({
|
||||||
|
VariableDeclaration(path: NodePath<t.VariableDeclaration>) {
|
||||||
|
// find the props destructuring, like const { prop1, prop2 } = props;
|
||||||
|
const declarations = path.get('declarations');
|
||||||
|
declarations.forEach(declaration => {
|
||||||
|
const init = declaration.get('init');
|
||||||
|
const id = declaration.get('id');
|
||||||
|
if (init.isIdentifier() && init.node.name === propsName) {
|
||||||
|
if (id.isObjectPattern()) {
|
||||||
|
props = id.get('properties').map(prop => parseSingleProp(prop));
|
||||||
|
} else if (id.isIdentifier()) {
|
||||||
|
props = extractPropsDestructing(fnPath, id.node.name);
|
||||||
|
}
|
||||||
|
// delete the declaration
|
||||||
|
if (props.length > 0) {
|
||||||
|
path.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The props format plugin, which is used to format the props of the component
|
||||||
|
* Goal: turn every pattern of props into a standard format
|
||||||
|
*
|
||||||
|
* 1. Nested props
|
||||||
|
* 2. props.xxx
|
||||||
|
*
|
||||||
|
* @param api
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
|
register(api);
|
||||||
|
let props: Prop[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
CallExpression(path: NodePath<t.CallExpression>) {
|
||||||
|
if (isCompPath(path)) {
|
||||||
|
const fnPath = extractFnFromMacro(path, COMPONENT) as
|
||||||
|
| NodePath<t.FunctionExpression>
|
||||||
|
| NodePath<ArrowFunctionWithBlock>;
|
||||||
|
// --- transform the props ---
|
||||||
|
if (fnPath.isArrowFunctionExpression()) {
|
||||||
|
wrapArrowFunctionWithBlock(fnPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- analyze the function props ---
|
||||||
|
const params = fnPath.get('params') as NodePath<t.Identifier | t.RestElement | t.Pattern>[];
|
||||||
|
if (params.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsPath = params[0];
|
||||||
|
if (propsPath) {
|
||||||
|
if (propsPath.isObjectPattern()) {
|
||||||
|
// --- object destructuring ---
|
||||||
|
props = propsPath.get('properties').map(prop => parseSingleProp(prop));
|
||||||
|
} else if (propsPath.isIdentifier()) {
|
||||||
|
props = extractPropsDestructing(fnPath, propsPath.node.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fnPath.node.body.body.unshift(...props.flatMap(prop => createPropAssignment(prop)));
|
||||||
|
|
||||||
|
// --- clear the props ---
|
||||||
|
fnPath.node.params = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSingleProp(path: NodePath<t.ObjectProperty | t.RestElement>): Prop {
|
||||||
|
if (path.isObjectProperty()) {
|
||||||
|
// --- normal property ---
|
||||||
|
const key = path.node.key;
|
||||||
|
const value = path.node.value;
|
||||||
|
if (t.isIdentifier(key) || t.isStringLiteral(key)) {
|
||||||
|
const name = t.isIdentifier(key) ? key.name : key.value;
|
||||||
|
return analyzeNestedProp(value, name, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Error(`Unsupported key type in object destructuring: ${key.type}`);
|
||||||
|
} else {
|
||||||
|
// --- rest element ---
|
||||||
|
const arg = path.get('argument');
|
||||||
|
if (!Array.isArray(arg) && arg.isIdentifier()) {
|
||||||
|
return {
|
||||||
|
type: PropType.REST,
|
||||||
|
name: arg.node.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw Error('Unsupported rest element type in object destructuring');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyzeNestedProp(value: t.ObjectProperty['value'], name: string, path: NodePath<t.ObjectProperty>): Prop {
|
||||||
|
let defaultVal: t.Expression | null = null;
|
||||||
|
let alias: string | null = null;
|
||||||
|
const nestedProps: string[] | null = [];
|
||||||
|
let nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null;
|
||||||
|
if (t.isIdentifier(value)) {
|
||||||
|
// 1. handle alias without default value
|
||||||
|
// handle alias without default value
|
||||||
|
if (name !== value.name) {
|
||||||
|
alias = value.name;
|
||||||
|
}
|
||||||
|
} else if (t.isAssignmentPattern(value)) {
|
||||||
|
// 2. handle default value case
|
||||||
|
const assignedName = value.left;
|
||||||
|
defaultVal = value.right;
|
||||||
|
if (t.isIdentifier(assignedName)) {
|
||||||
|
if (assignedName.name !== name) {
|
||||||
|
// handle alias in default value case
|
||||||
|
alias = assignedName.name;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Error(`Unsupported assignment type in object destructuring: ${assignedName.type}`);
|
||||||
|
}
|
||||||
|
} else if (t.isObjectPattern(value) || t.isArrayPattern(value)) {
|
||||||
|
nestedRelationship = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: PropType.SINGLE, name, defaultVal, alias, nestedProps, nestedRelationship };
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* 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 babel, { NodePath, PluginObj } from '@babel/core';
|
||||||
|
import type { DLightOption } from '../types';
|
||||||
|
import { register } from '@openinula/babel-api';
|
||||||
|
import { COMPONENT } from '../constants';
|
||||||
|
import { ArrowFunctionWithBlock, extractFnFromMacro, isCompPath, wrapArrowFunctionWithBlock } from '../utils';
|
||||||
|
import { types as t } from '@openinula/babel-api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state deconstructing plugin is used to transform the state deconstructing in the component body
|
||||||
|
* let { a, b } = props;
|
||||||
|
* // turn into
|
||||||
|
* let a, b;
|
||||||
|
* watch(() => {
|
||||||
|
* { a, b } = props;
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* @param api
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||||
|
register(api);
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
CallExpression(path: NodePath<t.CallExpression>) {
|
||||||
|
if (isCompPath(path)) {
|
||||||
|
const fnPath = extractFnFromMacro(path, COMPONENT) as
|
||||||
|
| NodePath<t.FunctionExpression>
|
||||||
|
| NodePath<ArrowFunctionWithBlock>;
|
||||||
|
|
||||||
|
fnPath.traverse({
|
||||||
|
VariableDeclarator(path) {
|
||||||
|
const idPath = path.get('id');
|
||||||
|
const initNode = path.node.init;
|
||||||
|
const nestedProps: string[] | null = [];
|
||||||
|
|
||||||
|
if (initNode && (idPath.isObjectPattern() || idPath.isArrayPattern())) {
|
||||||
|
// nested destructuring, collect the identifier that can be used in the function body as the prop
|
||||||
|
// e.g. function ({prop1, prop2: [p20X, {p211, p212: p212X}]}
|
||||||
|
// we should collect prop1, p20X, p211, p212X
|
||||||
|
idPath.traverse({
|
||||||
|
Identifier(path) {
|
||||||
|
// judge if the identifier is a prop
|
||||||
|
// 1. is the key of the object property and doesn't have alias
|
||||||
|
// 2. is the item of the array pattern and doesn't have alias
|
||||||
|
// 3. is alias of the object property
|
||||||
|
const parentPath = path.parentPath;
|
||||||
|
if (parentPath.isObjectProperty() && path.parentKey === 'value') {
|
||||||
|
// collect alias of the object property
|
||||||
|
nestedProps.push(path.node.name);
|
||||||
|
} else if (
|
||||||
|
parentPath.isArrayPattern() ||
|
||||||
|
parentPath.isObjectPattern() ||
|
||||||
|
parentPath.isRestElement() ||
|
||||||
|
(parentPath.isAssignmentPattern() && path.key === 'left')
|
||||||
|
) {
|
||||||
|
// collect the key of the object property or the item of the array pattern
|
||||||
|
nestedProps.push(path.node.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nestedProps.length) {
|
||||||
|
// declare the nested props as the variable
|
||||||
|
const declarationPath = path.parentPath.insertAfter(
|
||||||
|
t.variableDeclaration(
|
||||||
|
'let',
|
||||||
|
nestedProps.map(prop => t.variableDeclarator(t.identifier(prop)))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// move the deconstructing assignment into the watch function
|
||||||
|
declarationPath[0].insertAfter(
|
||||||
|
t.callExpression(t.identifier('watch'), [
|
||||||
|
t.arrowFunctionExpression(
|
||||||
|
[],
|
||||||
|
t.blockStatement([t.expressionStatement(t.assignmentExpression('=', idPath.node, initNode))])
|
||||||
|
),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
path.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,7 +1,11 @@
|
||||||
import { NodePath } from '@babel/core';
|
import { NodePath } from '@babel/core';
|
||||||
import * as t from '@babel/types';
|
import * as t from '@babel/types';
|
||||||
|
import { COMPONENT } from './constants';
|
||||||
|
|
||||||
export function extractFnFromMacro(path: NodePath<t.CallExpression>, macroName: string) {
|
export function extractFnFromMacro(
|
||||||
|
path: NodePath<t.CallExpression>,
|
||||||
|
macroName: string
|
||||||
|
): NodePath<t.FunctionExpression> | NodePath<t.ArrowFunctionExpression> {
|
||||||
const args = path.get('arguments');
|
const args = path.get('arguments');
|
||||||
|
|
||||||
const fnNode = args[0];
|
const fnNode = args[0];
|
||||||
|
@ -12,7 +16,11 @@ export function extractFnFromMacro(path: NodePath<t.CallExpression>, macroName:
|
||||||
throw new Error(`${macroName} macro must have a function argument`);
|
throw new Error(`${macroName} macro must have a function argument`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFnBody(path: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>) {
|
export function isFnExp(node: t.Node | null | undefined): node is t.FunctionExpression | t.ArrowFunctionExpression {
|
||||||
|
return t.isFunctionExpression(node) || t.isArrowFunctionExpression(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFnBodyPath(path: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>) {
|
||||||
const fnBody = path.get('body');
|
const fnBody = path.get('body');
|
||||||
if (fnBody.isExpression()) {
|
if (fnBody.isExpression()) {
|
||||||
// turn expression into block statement for consistency
|
// turn expression into block statement for consistency
|
||||||
|
@ -21,3 +29,40 @@ export function getFnBody(path: NodePath<t.FunctionExpression | t.ArrowFunctionE
|
||||||
|
|
||||||
return fnBody as unknown as NodePath<t.BlockStatement>;
|
return fnBody as unknown as NodePath<t.BlockStatement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFnBodyNode(node: t.FunctionExpression | t.ArrowFunctionExpression) {
|
||||||
|
const fnBody = node.body;
|
||||||
|
if (t.isExpression(fnBody)) {
|
||||||
|
// turn expression into block statement for consistency
|
||||||
|
return t.blockStatement([t.returnStatement(fnBody)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fnBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCompPath(path: NodePath<t.CallExpression>) {
|
||||||
|
// find the component, like: Component(() => {})
|
||||||
|
const callee = path.get('callee');
|
||||||
|
return callee.isIdentifier() && callee.node.name === COMPONENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArrowFunctionWithBlock extends t.ArrowFunctionExpression {
|
||||||
|
body: t.BlockStatement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wrapArrowFunctionWithBlock(path: NodePath<t.ArrowFunctionExpression>): ArrowFunctionWithBlock {
|
||||||
|
const { node } = path;
|
||||||
|
if (node.body.type !== 'BlockStatement') {
|
||||||
|
node.body = t.blockStatement([t.returnStatement(node.body)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node as ArrowFunctionWithBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMacroNode(fnBody: t.BlockStatement, macroName: string) {
|
||||||
|
return t.callExpression(t.identifier(macroName), [t.arrowFunctionExpression([], fnBody)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidPath<T>(path: NodePath<T>): path is NodePath<Exclude<T, undefined | null>> {
|
||||||
|
return !!path.node;
|
||||||
|
}
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { genCode, mockAnalyze } from '../mock';
|
|
||||||
import { PropType } from '../../src/constants';
|
|
||||||
import { propsAnalyze } from '../../src/analyzer/propsAnalyze';
|
|
||||||
|
|
||||||
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze]);
|
|
||||||
|
|
||||||
describe('analyze props', () => {
|
|
||||||
it('should work', () => {
|
|
||||||
const root = analyze(/*js*/ `
|
|
||||||
Component(({foo, bar}) => {})
|
|
||||||
`);
|
|
||||||
expect(root.props.length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support default value', () => {
|
|
||||||
const root = analyze(/*js*/ `
|
|
||||||
Component(({foo = 'default', bar = 123}) => {})
|
|
||||||
`);
|
|
||||||
expect(root.props.length).toBe(2);
|
|
||||||
expect(root.props[0].name).toBe('foo');
|
|
||||||
expect(root.props[1].name).toBe('bar');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support alias', () => {
|
|
||||||
const root = analyze(/*js*/ `
|
|
||||||
Component(({'foo': renamed, bar: anotherName}) => {})
|
|
||||||
`);
|
|
||||||
expect(root.props.length).toBe(2);
|
|
||||||
expect(root.props[0].name).toBe('foo');
|
|
||||||
expect(root.props[0].alias).toBe('renamed');
|
|
||||||
expect(root.props[1].name).toBe('bar');
|
|
||||||
expect(root.props[1].alias).toBe('anotherName');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support nested props', () => {
|
|
||||||
const root = analyze(/*js*/ `
|
|
||||||
Component(({foo: {nested1, nested2}, bar}) => {})
|
|
||||||
`);
|
|
||||||
expect(root.props.length).toBe(2);
|
|
||||||
expect(root.props[0].name).toBe('foo');
|
|
||||||
expect(root.props[0].nestedProps).toEqual(['nested1', 'nested2']);
|
|
||||||
expect(genCode(root.props[0].nestedRelationship)).toMatchInlineSnapshot(`
|
|
||||||
"{
|
|
||||||
nested1,
|
|
||||||
nested2
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
expect(root.props[1].name).toBe('bar');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support complex nested props', () => {
|
|
||||||
// language=js
|
|
||||||
const root = analyze(/*js*/ `
|
|
||||||
Component(function ({
|
|
||||||
prop1, prop2: {p2: [p20X = defaultVal, {p211, p212: p212X = defaultVal}, ...restArr], p3, ...restObj}}
|
|
||||||
) {});
|
|
||||||
`);
|
|
||||||
// we should collect prop1, p20X, p211, p212X, p3
|
|
||||||
expect(root.props.length).toBe(2);
|
|
||||||
expect(root.props[0].name).toBe('prop1');
|
|
||||||
expect(root.props[1].name).toBe('prop2');
|
|
||||||
expect(root.props[1].nestedProps).toEqual(['p20X', 'p211', 'p212X', 'restArr', 'p3', 'restObj']);
|
|
||||||
expect(genCode(root.props[1].nestedRelationship)).toMatchInlineSnapshot(`
|
|
||||||
"{
|
|
||||||
p2: [p20X = defaultVal, {
|
|
||||||
p211,
|
|
||||||
p212: p212X = defaultVal
|
|
||||||
}, ...restArr],
|
|
||||||
p3,
|
|
||||||
...restObj
|
|
||||||
}"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support rest element', () => {
|
|
||||||
const root = analyze(/*js*/ `
|
|
||||||
Component(({foo, ...rest}) => {})
|
|
||||||
`);
|
|
||||||
expect(root.props.length).toBe(2);
|
|
||||||
expect(root.props[0].name).toBe('foo');
|
|
||||||
expect(root.props[0].type).toBe(PropType.SINGLE);
|
|
||||||
expect(root.props[1].name).toBe('rest');
|
|
||||||
expect(root.props[1].type).toBe(PropType.REST);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support empty props', () => {
|
|
||||||
const root = analyze(/*js*/ `
|
|
||||||
Component(() => {})
|
|
||||||
`);
|
|
||||||
expect(root.props.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* 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 { compile } from './mock';
|
||||||
|
import autoNamingPlugin from '../../src/sugarPlugins/autoNamingPlugin';
|
||||||
|
|
||||||
|
const mock = (code: string) => compile([autoNamingPlugin], code);
|
||||||
|
|
||||||
|
describe('auto naming', () => {
|
||||||
|
describe('component', () => {
|
||||||
|
it('should transform FunctionDeclaration into Component macro', () => {
|
||||||
|
const code = `
|
||||||
|
function MyComponent() {}
|
||||||
|
`;
|
||||||
|
const transformedCode = mock(code);
|
||||||
|
expect(transformedCode).toMatchInlineSnapshot(`
|
||||||
|
"const MyComponent = Component(() => {});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform VariableDeclaration with function expression into Component macro', () => {
|
||||||
|
const code = `
|
||||||
|
const MyComponent = function() {}
|
||||||
|
`;
|
||||||
|
const transformedCode = mock(code);
|
||||||
|
expect(transformedCode).toMatchInlineSnapshot(`
|
||||||
|
"const MyComponent = Component(() => {});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform VariableDeclaration with arrow function into Component macro', () => {
|
||||||
|
const code = `
|
||||||
|
const MyComponent = () => {}
|
||||||
|
`;
|
||||||
|
const transformedCode = mock(code);
|
||||||
|
expect(transformedCode).toMatchInlineSnapshot(`
|
||||||
|
"const MyComponent = Component(() => {});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform inner function into Component macro', () => {
|
||||||
|
const code = `
|
||||||
|
function MyComponent() {
|
||||||
|
function Inner() {}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const transformedCode = mock(code);
|
||||||
|
expect(transformedCode).toMatchInlineSnapshot(`
|
||||||
|
"const MyComponent = Component(() => {
|
||||||
|
const Inner = Component(() => {});
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hook', () => {
|
||||||
|
it('should transform FunctionDeclaration into Hook macro', () => {
|
||||||
|
const code = `
|
||||||
|
function useMyHook() {}
|
||||||
|
`;
|
||||||
|
const transformedCode = mock(code);
|
||||||
|
expect(transformedCode).toMatchInlineSnapshot(`
|
||||||
|
"const useMyHook = Hook(() => {});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform VariableDeclaration with function expression into Hook macro', () => {
|
||||||
|
const code = `
|
||||||
|
const useMyHook = function() {}
|
||||||
|
`;
|
||||||
|
const transformedCode = mock(code);
|
||||||
|
expect(transformedCode).toMatchInlineSnapshot(`
|
||||||
|
"const useMyHook = Hook(() => {});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform VariableDeclaration with arrow function into Hook macro', () => {
|
||||||
|
const code = `
|
||||||
|
const useMyHook = () => {}
|
||||||
|
`;
|
||||||
|
const transformedCode = mock(code);
|
||||||
|
expect(transformedCode).toMatchInlineSnapshot(`
|
||||||
|
"const useMyHook = Hook(() => {});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('invalid case', () => {
|
||||||
|
it('should not transform FunctionDeclaration with invalid name', () => {
|
||||||
|
const code = `
|
||||||
|
function myComponent() {}
|
||||||
|
`;
|
||||||
|
const transformedCode = mock(code);
|
||||||
|
expect(transformedCode).toMatchInlineSnapshot(`
|
||||||
|
"function myComponent() {}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not transform VariableDeclaration with invalid name', () => {
|
||||||
|
const code = `
|
||||||
|
const myComponent = function() {}
|
||||||
|
`;
|
||||||
|
const transformedCode = mock(code);
|
||||||
|
expect(transformedCode).toMatchInlineSnapshot(`"const myComponent = function () {};"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not transform VariableDeclaration with invalid arrow function', () => {
|
||||||
|
const code = `
|
||||||
|
const myComponent = () => {}
|
||||||
|
`;
|
||||||
|
const transformedCode = mock(code);
|
||||||
|
expect(transformedCode).toMatchInlineSnapshot(`"const myComponent = () => {};"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* 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 { compile } from './mock';
|
||||||
|
import stateDeconstructingPlugin from '../../src/sugarPlugins/stateDeconstructingPlugin';
|
||||||
|
|
||||||
|
const mock = (code: string) => compile([stateDeconstructingPlugin], code);
|
||||||
|
|
||||||
|
describe('state deconstructing', () => {
|
||||||
|
it('should work with object deconstructing', () => {
|
||||||
|
expect(
|
||||||
|
mock(`
|
||||||
|
Component(() => {
|
||||||
|
const { a, b } = c_$$props;
|
||||||
|
const { e, f } = g();
|
||||||
|
})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"Component(() => {
|
||||||
|
let a, b;
|
||||||
|
watch(() => {
|
||||||
|
({
|
||||||
|
a,
|
||||||
|
b
|
||||||
|
} = c_$$props);
|
||||||
|
})
|
||||||
|
let e, f;
|
||||||
|
watch(() => {
|
||||||
|
({
|
||||||
|
e,
|
||||||
|
f
|
||||||
|
} = g());
|
||||||
|
})
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with array deconstructing', () => {
|
||||||
|
expect(
|
||||||
|
mock(`
|
||||||
|
Component(() => {
|
||||||
|
const [a, b] = c_$$props
|
||||||
|
})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"Component(() => {
|
||||||
|
let a, b;
|
||||||
|
watch(() => {
|
||||||
|
[a, b] = c_$$props;
|
||||||
|
})
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support nested deconstructing', () => {
|
||||||
|
// language=js
|
||||||
|
expect(
|
||||||
|
mock(/*js*/ `
|
||||||
|
Component(() => {
|
||||||
|
const {
|
||||||
|
p2: [p20X = defaultVal, {p211, p212: p212X = defaultVal}, ...restArr],
|
||||||
|
p3,
|
||||||
|
...restObj
|
||||||
|
} = prop2_$$prop;
|
||||||
|
});
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"Component(() => {
|
||||||
|
let p20X, p211, p212X, restArr, p3, restObj;
|
||||||
|
watch(() => {
|
||||||
|
({
|
||||||
|
p2: [p20X = defaultVal, {
|
||||||
|
p211,
|
||||||
|
p212: p212X = defaultVal
|
||||||
|
}, ...restArr],
|
||||||
|
p3,
|
||||||
|
...restObj
|
||||||
|
} = prop2_$$prop);
|
||||||
|
})
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* 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 { compile } from './mock';
|
||||||
|
import earlyReturnPlugin from '../../src/sugarPlugins/earlyReturnPlugin';
|
||||||
|
|
||||||
|
const mock = (code: string) => compile([earlyReturnPlugin], code);
|
||||||
|
|
||||||
|
describe('analyze early return', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(
|
||||||
|
mock(/*js*/ `
|
||||||
|
const App = Component(() => {
|
||||||
|
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>;
|
||||||
|
})`)
|
||||||
|
).toMatchInlineSnapshot(/*js*/ `
|
||||||
|
"const App = Component(() => {
|
||||||
|
const Branch_1 = Component(() => {
|
||||||
|
return <div>1</div>;
|
||||||
|
});
|
||||||
|
const Default_1 = Component(() => {
|
||||||
|
return <div>
|
||||||
|
<if cond={count > 1}>{count} is bigger than is 1</if>
|
||||||
|
<else>{count} is smaller than 1</else>
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
return <><if cond={count > 1}><Branch_1 /></if><else><Default_1 /></else></>;
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with multi if', () => {
|
||||||
|
expect(
|
||||||
|
mock(/*js*/ `
|
||||||
|
const App = Component(() => {
|
||||||
|
if (count > 1) {
|
||||||
|
return <div>1</div>
|
||||||
|
}
|
||||||
|
if (count > 2) {
|
||||||
|
return <div>2</div>
|
||||||
|
}
|
||||||
|
return <div></div>;
|
||||||
|
})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(/*js*/ `
|
||||||
|
"const App = Component(() => {
|
||||||
|
const Branch_1 = Component(() => {
|
||||||
|
return <div>1</div>;
|
||||||
|
});
|
||||||
|
const Default_1 = Component(() => {
|
||||||
|
const Branch_2 = Component(() => {
|
||||||
|
return <div>2</div>;
|
||||||
|
});
|
||||||
|
const Default_2 = Component(() => {
|
||||||
|
return <div></div>;
|
||||||
|
});
|
||||||
|
return <><if cond={count > 2}><Branch_2 /></if><else><Default_2 /></else></>;
|
||||||
|
});
|
||||||
|
return <><if cond={count > 1}><Branch_1 /></if><else><Default_1 /></else></>;
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with nested if', () => {
|
||||||
|
expect(
|
||||||
|
mock(/*js*/ `
|
||||||
|
const App = Component(() => {
|
||||||
|
if (count > 1) {
|
||||||
|
if (count > 2) {
|
||||||
|
return <div>2</div>
|
||||||
|
}
|
||||||
|
return <div>1</div>
|
||||||
|
}
|
||||||
|
return <div></div>;
|
||||||
|
})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(/*js*/ `
|
||||||
|
"const App = Component(() => {
|
||||||
|
const Branch_1 = Component(() => {
|
||||||
|
const Branch_2 = Component(() => {
|
||||||
|
return <div>2</div>;
|
||||||
|
});
|
||||||
|
const Default_2 = Component(() => {
|
||||||
|
return <div>1</div>;
|
||||||
|
});
|
||||||
|
return <><if cond={count > 2}><Branch_2 /></if><else><Default_2 /></else></>;
|
||||||
|
});
|
||||||
|
const Default_1 = Component(() => {
|
||||||
|
return <div></div>;
|
||||||
|
});
|
||||||
|
return <><if cond={count > 1}><Branch_1 /></if><else><Default_1 /></else></>;
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,87 +0,0 @@
|
||||||
/*
|
|
||||||
* 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/analyzer';
|
|
||||||
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?.children;
|
|
||||||
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?.children;
|
|
||||||
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?.children;
|
|
||||||
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,86 @@
|
||||||
|
/*
|
||||||
|
* 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 { compile } from './mock';
|
||||||
|
import jsxSlicePlugin from '../../src/sugarPlugins/jsxSlicePlugin';
|
||||||
|
|
||||||
|
const mock = (code: string) => compile([jsxSlicePlugin], code);
|
||||||
|
|
||||||
|
describe('jsx slice', () => {
|
||||||
|
it('should work with jsx slice', () => {
|
||||||
|
expect(
|
||||||
|
mock(`
|
||||||
|
function App() {
|
||||||
|
const a = <div></div>
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(/*jsx*/ `
|
||||||
|
"function App() {
|
||||||
|
const a = Component(() => <div></div>);
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with jsx slice in ternary operator', () => {
|
||||||
|
expect(
|
||||||
|
mock(`
|
||||||
|
function App() {
|
||||||
|
const a = true ? <Table.Col></Table.Col> : <div></div>
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"function App() {
|
||||||
|
const JSX_Table_Col = Component(() => <Table.Col></Table.Col>);
|
||||||
|
const JSX_div = Component(() => <div></div>);
|
||||||
|
const a = true ? <JSX_Table_Col /> : <JSX_div />;
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with jsx slice in arr', () => {
|
||||||
|
expect(
|
||||||
|
mock(`
|
||||||
|
function App() {
|
||||||
|
const arr = [<div></div>,<h1></h1>]
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"function App() {
|
||||||
|
const JSX_div = Component(() => <div></div>);
|
||||||
|
const JSX_h = Component(() => <h1></h1>);
|
||||||
|
const arr = [<JSX_div />, <JSX_h />];
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fragment should work', () => {
|
||||||
|
expect(
|
||||||
|
mock(`
|
||||||
|
function App() {
|
||||||
|
const a = <>{test}</>
|
||||||
|
const b = cond ? <><div></div></> : <><span></span></>
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"function App() {
|
||||||
|
const a = Component(() => <>{test}</>);
|
||||||
|
const JSX_Fragment = Component(() => <><div></div></>);
|
||||||
|
const JSX_Fragment2 = Component(() => <><span></span></>);
|
||||||
|
const b = cond ? <JSX_Fragment /> : <JSX_Fragment2 />;
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,32 @@
|
||||||
|
/*
|
||||||
|
* 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 { type PluginItem, transform as transformWithBabel } from '@babel/core';
|
||||||
|
import syntaxJSX from '@babel/plugin-syntax-jsx';
|
||||||
|
import { register } from '@openinula/babel-api';
|
||||||
|
|
||||||
|
export function compile(plugins: PluginItem[], code: string) {
|
||||||
|
return transformWithBabel(code, {
|
||||||
|
plugins: [
|
||||||
|
syntaxJSX.default ?? syntaxJSX,
|
||||||
|
function (api) {
|
||||||
|
register(api);
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
...plugins,
|
||||||
|
],
|
||||||
|
filename: 'test.tsx',
|
||||||
|
})?.code;
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* 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 { compile } from './mock';
|
||||||
|
import propsFormatPlugin from '../../src/sugarPlugins/propsFormatPlugin';
|
||||||
|
|
||||||
|
const mock = (code: string) => compile([propsFormatPlugin], code);
|
||||||
|
|
||||||
|
describe('analyze props', () => {
|
||||||
|
describe('props in params', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(
|
||||||
|
mock(`
|
||||||
|
Component(({foo, bar}) => {})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"Component(() => {
|
||||||
|
let foo_$$prop;
|
||||||
|
let bar_$$prop;
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support default value', () => {
|
||||||
|
expect(
|
||||||
|
mock(`
|
||||||
|
Component(({foo = 'default', bar = 123}) => {})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"Component(() => {
|
||||||
|
let foo_$$prop = 'default';
|
||||||
|
let bar_$$prop = 123;
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support alias', () => {
|
||||||
|
expect(
|
||||||
|
mock(/*js*/ `
|
||||||
|
Component(({'foo': renamed, bar: anotherName}) => {})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"Component(() => {
|
||||||
|
let foo_$$prop;
|
||||||
|
let renamed = foo_$$prop;
|
||||||
|
let bar_$$prop;
|
||||||
|
let anotherName = bar_$$prop;
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support nested props', () => {
|
||||||
|
expect(
|
||||||
|
mock(/*js*/ `
|
||||||
|
Component(({foo: {nested1, nested2}, bar}) => {})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"Component(() => {
|
||||||
|
let foo_$$prop;
|
||||||
|
let {
|
||||||
|
nested1,
|
||||||
|
nested2
|
||||||
|
} = foo_$$prop;
|
||||||
|
let bar_$$prop;
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
//
|
||||||
|
it('should support complex nested props', () => {
|
||||||
|
// language=js
|
||||||
|
expect(
|
||||||
|
mock(/*js*/ `
|
||||||
|
Component(function ({
|
||||||
|
prop1,
|
||||||
|
prop2: {
|
||||||
|
p2: [p20X = defaultVal, {p211, p212: p212X = defaultVal}, ...restArr],
|
||||||
|
p3,
|
||||||
|
...restObj
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
});
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"Component(function () {
|
||||||
|
let prop1_$$prop;
|
||||||
|
let prop2_$$prop;
|
||||||
|
let {
|
||||||
|
p2: [p20X = defaultVal, {
|
||||||
|
p211,
|
||||||
|
p212: p212X = defaultVal
|
||||||
|
}, ...restArr],
|
||||||
|
p3,
|
||||||
|
...restObj
|
||||||
|
} = prop2_$$prop;
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support rest element', () => {
|
||||||
|
expect(
|
||||||
|
mock(/*js*/ `
|
||||||
|
Component(({foo, ...rest}) => {})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"Component(() => {
|
||||||
|
let foo_$$prop;
|
||||||
|
let rest_$$prop;
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support empty props', () => {
|
||||||
|
expect(
|
||||||
|
mock(/*js*/ `
|
||||||
|
Component(() => {})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`"Component(() => {});"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('props in variable declaration', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(
|
||||||
|
mock(/*js*/ `
|
||||||
|
Component((props) => {
|
||||||
|
const {foo, bar} = props;
|
||||||
|
})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"Component(() => {
|
||||||
|
let foo_$$prop;
|
||||||
|
let bar_$$prop;
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support props renaming', () => {
|
||||||
|
expect(
|
||||||
|
mock(/*js*/ `
|
||||||
|
Component((props) => {
|
||||||
|
const newProps = props;
|
||||||
|
const {foo: renamed, bar} = newProps;
|
||||||
|
})
|
||||||
|
`)
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"Component(() => {
|
||||||
|
let foo_$$prop;
|
||||||
|
let renamed = foo_$$prop;
|
||||||
|
let bar_$$prop;
|
||||||
|
});"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue