feat: autoNaming, autoReturn, deconstructing, jsxSlice and props

This commit is contained in:
Hoikan 2024-05-14 21:03:38 +08:00
parent 4ca2d66fac
commit dabc0eed20
21 changed files with 1245 additions and 378 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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$/>
// }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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