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 { types as t } from '@openinula/babel-api';
|
||||
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';
|
||||
|
||||
function isLifeCycleName(name: string): name is LifeCycle {
|
||||
|
@ -44,7 +44,7 @@ export function functionalMacroAnalyze(): Visitor {
|
|||
// lifecycle
|
||||
if (isLifeCycleName(calleeName)) {
|
||||
const fnNode = extractFnFromMacro(expression, calleeName);
|
||||
addLifecycle(ctx.current, calleeName, getFnBody(fnNode).node);
|
||||
addLifecycle(ctx.current, calleeName, getFnBodyPath(fnNode).node);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { type NodePath } from '@babel/core';
|
||||
import { propsAnalyze } from './propsAnalyze';
|
||||
import { AnalyzeContext, Analyzer, ComponentNode, Visitor } from './types';
|
||||
import { addLifecycle, createComponentNode } from './nodeFactory';
|
||||
import { variablesAnalyze } from './variablesAnalyze';
|
||||
import { functionalMacroAnalyze } from './functionalMacroAnalyze';
|
||||
import { getFnBody } from '../utils';
|
||||
import { getFnBodyPath } from '../utils';
|
||||
import { viewAnalyze } from './viewAnalyze';
|
||||
import { WILL_MOUNT } from '../constants';
|
||||
import { types as t } from '@openinula/babel-api';
|
||||
const builtinAnalyzers = [propsAnalyze, variablesAnalyze, functionalMacroAnalyze, viewAnalyze];
|
||||
const builtinAnalyzers = [variablesAnalyze, functionalMacroAnalyze, viewAnalyze];
|
||||
|
||||
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
||||
return visitors.reduce<Visitor<AnalyzeContext>>((acc, cur) => {
|
||||
|
@ -50,7 +49,7 @@ export function analyzeFnComp(
|
|||
}
|
||||
|
||||
// --- analyze the function body ---
|
||||
const bodyStatements = getFnBody(fnNode).get('body');
|
||||
const bodyStatements = getFnBodyPath(fnNode).get('body');
|
||||
for (let i = 0; i < bodyStatements.length; i++) {
|
||||
const p = bodyStatements[i];
|
||||
|
||||
|
|
|
@ -13,8 +13,10 @@
|
|||
* 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 { ArrowFunctionWithBlock } from '../utils';
|
||||
|
||||
export function isValidPath<T>(path: NodePath<T>): path is NodePath<Exclude<T, undefined | null>> {
|
||||
return !!path.node;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export const COMPONENT = 'Component';
|
||||
export const Hook = 'Hook';
|
||||
export const WILL_MOUNT = 'willMount';
|
||||
export const ON_MOUNT = 'onMount';
|
||||
export const WILL_UNMOUNT = 'willUnmount';
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
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 { defaultAttributeMap, defaultHTMLTags } from './const';
|
||||
import { analyze } from './analyzer';
|
||||
import { NodePath, type types as t } from '@babel/core';
|
||||
import { COMPONENT } from './constants';
|
||||
import { extractFnFromMacro } from './utils';
|
||||
import { extractFnFromMacro, isCompPath } from './utils';
|
||||
import { register } from '@openinula/babel-api';
|
||||
|
||||
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>) {
|
||||
// find the component, like: Component(() => {})
|
||||
const callee = path.get('callee');
|
||||
|
||||
if (callee.isIdentifier() && callee.node.name === COMPONENT) {
|
||||
if (isCompPath(path)) {
|
||||
const componentNode = extractFnFromMacro(path, COMPONENT);
|
||||
let name = '';
|
||||
// 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.
|
||||
*/
|
||||
|
||||
import { NodePath, type types as t } from '@babel/core';
|
||||
import { createComponentNode, createCondNode, createJSXNode } from '../analyze/nodeFactory';
|
||||
import { AnalyzeContext, Branch, Visitor } from '../analyze/types';
|
||||
import { isValidPath } from '../analyze/utils';
|
||||
import babel, { NodePath, PluginObj } from '@babel/core';
|
||||
import { register, types as t } from '@openinula/babel-api';
|
||||
import { isValidPath } from '../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 {
|
||||
ReturnStatement(path: NodePath<t.ReturnStatement>, context: AnalyzeContext) {
|
||||
const currentComp = context.current;
|
||||
visitor: {
|
||||
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');
|
||||
if (argument.isJSXElement()) {
|
||||
currentComp.children = createJSXNode(currentComp, argument);
|
||||
}
|
||||
},
|
||||
IfStatement(ifStmt: NodePath<t.IfStatement>, context: AnalyzeContext) {
|
||||
if (!hasEarlyReturn(ifStmt)) {
|
||||
return;
|
||||
}
|
||||
const currentComp = context.current;
|
||||
const branches = parseBranches(
|
||||
bodyPath.get('body')[ifStmtIndex] as NodePath<t.IfStatement>,
|
||||
bodyPath.node.body.slice(ifStmtIndex + 1)
|
||||
);
|
||||
|
||||
const branches: Branch[] = [];
|
||||
let next: NodePath<t.Statement> | null = ifStmt;
|
||||
let branchIdx = 0;
|
||||
// At first, we remove the node after the if statement in the function body
|
||||
let i = bodyPath.node.body.length - 1;
|
||||
while (i >= ifStmtIndex) {
|
||||
bodyPath.get('body')[i].remove();
|
||||
i--;
|
||||
}
|
||||
|
||||
// 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),
|
||||
});
|
||||
// Then we generate the every brach component
|
||||
const branchNodes = branches.map(branch =>
|
||||
t.variableDeclaration('const', [t.variableDeclarator(t.identifier(branch.name), branch.content)])
|
||||
);
|
||||
// push the branch components to the function body
|
||||
bodyPath.pushContainer('body', branchNodes);
|
||||
|
||||
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.children = createCondNode(currentComp, defaultComponent, branches);
|
||||
// At last, we generate the cond node
|
||||
const condNode = generateCondNode(branches);
|
||||
bodyPath.pushContainer('body', t.returnStatement(condNode));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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>) {
|
||||
return next.isBlockStatement() ? next.get('body') : [next];
|
||||
return next.isBlockStatement() ? next.node.body : [next.node];
|
||||
}
|
||||
|
||||
function hasEarlyReturn(path: NodePath<t.Node>) {
|
||||
|
@ -88,3 +152,23 @@ function hasEarlyReturn(path: NodePath<t.Node>) {
|
|||
});
|
||||
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.
|
||||
*/
|
||||
|
||||
import { NodePath } from '@babel/core';
|
||||
import { AnalyzeContext, Visitor } from '../analyze/types';
|
||||
import { createSubCompNode } from '../analyze/nodeFactory';
|
||||
import babel, { NodePath, PluginObj } from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
import type { DLightOption } from '../types';
|
||||
import { register } from '@openinula/babel-api';
|
||||
|
||||
function genName(tagName: string, ctx: AnalyzeContext) {
|
||||
return `$$${tagName}-Sub${ctx.current.subComponents.length}`;
|
||||
}
|
||||
function transformJSXSlice(path: NodePath<t.JSXElement> | NodePath<t.JSXFragment>) {
|
||||
path.skip();
|
||||
|
||||
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);
|
||||
// don't handle the jsx in return statement or in the arrow function return
|
||||
if (path.parentPath.isReturnStatement() || path.parentPath.isArrowFunctionExpression()) {
|
||||
// skip the children
|
||||
return;
|
||||
}
|
||||
|
||||
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>) {
|
||||
// create a subComponent node and add it to the current component
|
||||
const subComp = createSubCompNode(name, ctx.current, path.node);
|
||||
ctx.current.subComponents.push(subComp);
|
||||
function genName(node: t.JSXElement | t.JSXFragment) {
|
||||
if (t.isJSXFragment(node)) {
|
||||
return 'Fragment';
|
||||
}
|
||||
|
||||
// replace with the subComp jsxElement
|
||||
const subCompJSX = t.jsxElement(
|
||||
t.jsxOpeningElement(t.jsxIdentifier(name), [], true),
|
||||
t.jsxClosingElement(t.jsxIdentifier(name)),
|
||||
[],
|
||||
true
|
||||
);
|
||||
path.replaceWith(subCompJSX);
|
||||
const jsxName = node.openingElement.name;
|
||||
if (t.isJSXIdentifier(jsxName)) {
|
||||
return jsxName.name;
|
||||
} else if (t.isJSXMemberExpression(jsxName)) {
|
||||
// connect all parts with _
|
||||
let result = jsxName.property.name;
|
||||
let current: t.JSXMemberExpression | t.JSXIdentifier = jsxName.object;
|
||||
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$/>
|
||||
* ```
|
||||
*/
|
||||
export function jsxSlicesAnalyze(): Visitor {
|
||||
export default function (api: typeof babel, options: DLightOption): PluginObj {
|
||||
register(api);
|
||||
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);
|
||||
visitor: {
|
||||
JSXElement(path: NodePath<t.JSXElement>) {
|
||||
transformJSXSlice(path);
|
||||
},
|
||||
JSXFragment(path: NodePath<t.JSXFragment>) {
|
||||
transformJSXSlice(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 * 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 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`);
|
||||
}
|
||||
|
||||
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');
|
||||
if (fnBody.isExpression()) {
|
||||
// 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>;
|
||||
}
|
||||
|
||||
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