diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts index 6cc96d3d..6feb09ee 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts @@ -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; } diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/index.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/index.ts index b183af2b..0b14b3d3 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/index.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/index.ts @@ -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>((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]; diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/utils.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/utils.ts index 5795e3ca..8d32241b 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/utils.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/utils.ts @@ -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(path: NodePath): path is NodePath> { return !!path.node; diff --git a/packages/transpiler/babel-inula-next-core/src/constants.ts b/packages/transpiler/babel-inula-next-core/src/constants.ts index 53ddcbca..6a4afc92 100644 --- a/packages/transpiler/babel-inula-next-core/src/constants.ts +++ b/packages/transpiler/babel-inula-next-core/src/constants.ts @@ -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'; diff --git a/packages/transpiler/babel-inula-next-core/src/plugin.ts b/packages/transpiler/babel-inula-next-core/src/plugin.ts index 62d5c493..830787f1 100644 --- a/packages/transpiler/babel-inula-next-core/src/plugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/plugin.ts @@ -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) { - // 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 diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/PropsFormatPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/PropsFormatPlugin.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/autoNamingPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/autoNamingPlugin.ts index 5de789bd..205f4c31 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/autoNamingPlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/autoNamingPlugin.ts @@ -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) { + const { id } = path.node; + const macroNode = getMacroNode(id, path.node.body); + if (macroNode) { + path.replaceWith(macroNode); + } + }, + VariableDeclaration(path: NodePath) { + 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); +} diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts index 10ac4ad3..eb74dc67 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts @@ -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 + * + * + * + * + * + * + * ``` + * @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 + 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, context: AnalyzeContext) { - const currentComp = context.current; + visitor: { + CallExpression(path: NodePath) { + 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, context: AnalyzeContext) { - if (!hasEarlyReturn(ifStmt)) { - return; - } - const currentComp = context.current; + const branches = parseBranches( + bodyPath.get('body')[ifStmtIndex] as NodePath, + bodyPath.node.body.slice(ifStmtIndex + 1) + ); - const branches: Branch[] = []; - let next: NodePath | 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 = 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, restStmt: t.Statement[]) { + const branches: Branch[] = []; + let next: NodePath | 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 = 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) { - return next.isBlockStatement() ? next.get('body') : [next]; + return next.isBlockStatement() ? next.node.body : [next.node]; } function hasEarlyReturn(path: NodePath) { @@ -88,3 +152,23 @@ function hasEarlyReturn(path: NodePath) { }); 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; +} diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts index b1683e4b..613f956c 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts @@ -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 | NodePath) { + path.skip(); -function genNameFromJSX(path: NodePath, 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 =
` + // transform it into: + // ```jsx + // const a = Component(() => { + // return
+ // }) + // ``` + if (path.parentPath.isVariableDeclarator()) { + path.replaceWith(sliceCompNode); + } else { + // extract the jsx slice into a subcomponent, like const a = type?
: + // transform it into: + // ```jsx + // const Div$$ = (() => { + // return
+ // }); + // const Span$$ = Component(() => { + // return + // }); + // const a = type? : ; + // ``` + 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) { - // 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 = * ``` */ -export function jsxSlicesAnalyze(): Visitor { +export default function (api: typeof babel, options: DLightOption): PluginObj { + register(api); return { - JSXElement(path: NodePath, ctx) { - const name = genNameFromJSX(path, ctx); - replaceJSXSliceWithSubComp(name, ctx, path); - path.skip(); - }, - JSXFragment(path: NodePath, ctx) { - replaceJSXSliceWithSubComp('frag', ctx, path); + visitor: { + JSXElement(path: NodePath) { + transformJSXSlice(path); + }, + JSXFragment(path: NodePath) { + transformJSXSlice(path); + }, }, }; } - -// Analyze the JSX slice in the function component, including: -// 1. VariableDeclaration, like `const a =
` -// 2. SubComponent, like `function Sub() { return
}` -function handleFn(fnName: string, fnBody: NodePath) { - 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
{count}
- // } - // => - // function jsxFunc() { - // function Comp_$id4$() { - // return
{count}
- // } - // // This is a function that returns JSX - // // because the function name is smallCamelCased - // return - // } - } -} diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/propsFormatPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/propsFormatPlugin.ts new file mode 100644 index 00000000..f77284e1 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/propsFormatPlugin.ts @@ -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 | NodePath, + propsName: string +) { + let props: Prop[] = []; + const body = fnPath.get('body') as NodePath; + body.traverse({ + VariableDeclaration(path: NodePath) { + // 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) { + if (isCompPath(path)) { + const fnPath = extractFnFromMacro(path, COMPONENT) as + | NodePath + | NodePath; + // --- transform the props --- + if (fnPath.isArrowFunctionExpression()) { + wrapArrowFunctionWithBlock(fnPath); + } + + // --- analyze the function props --- + const params = fnPath.get('params') as NodePath[]; + 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): 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): 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 }; +} diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/stateDeconstructingPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/stateDeconstructingPlugin.ts new file mode 100644 index 00000000..02249b6b --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/stateDeconstructingPlugin.ts @@ -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) { + if (isCompPath(path)) { + const fnPath = extractFnFromMacro(path, COMPONENT) as + | NodePath + | NodePath; + + 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(); + } + } + }, + }); + } + }, + }, + }; +} diff --git a/packages/transpiler/babel-inula-next-core/src/utils.ts b/packages/transpiler/babel-inula-next-core/src/utils.ts index a4596b5e..fe8fd12e 100644 --- a/packages/transpiler/babel-inula-next-core/src/utils.ts +++ b/packages/transpiler/babel-inula-next-core/src/utils.ts @@ -1,7 +1,11 @@ import { NodePath } from '@babel/core'; import * as t from '@babel/types'; +import { COMPONENT } from './constants'; -export function extractFnFromMacro(path: NodePath, macroName: string) { +export function extractFnFromMacro( + path: NodePath, + macroName: string +): NodePath | NodePath { const args = path.get('arguments'); const fnNode = args[0]; @@ -12,7 +16,11 @@ export function extractFnFromMacro(path: NodePath, macroName: throw new Error(`${macroName} macro must have a function argument`); } -export function getFnBody(path: NodePath) { +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) { const fnBody = path.get('body'); if (fnBody.isExpression()) { // turn expression into block statement for consistency @@ -21,3 +29,40 @@ export function getFnBody(path: NodePath; } + +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) { + // 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): 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(path: NodePath): path is NodePath> { + return !!path.node; +} diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/props.x.ts b/packages/transpiler/babel-inula-next-core/test/analyze/props.x.ts deleted file mode 100644 index 1caaab8a..00000000 --- a/packages/transpiler/babel-inula-next-core/test/analyze/props.x.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/autoNaming.test.tsx b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/autoNaming.test.tsx new file mode 100644 index 00000000..bb029882 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/autoNaming.test.tsx @@ -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 = () => {};"`); + }); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/deconstructing.test.tsx b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/deconstructing.test.tsx new file mode 100644 index 00000000..7073cbe4 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/deconstructing.test.tsx @@ -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); + }) + });" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/earlyReturn.test.tsx b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/earlyReturn.test.tsx new file mode 100644 index 00000000..763c8158 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/earlyReturn.test.tsx @@ -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
1
+ } + return
+ 1}>{count} is bigger than is 1 + {count} is smaller than 1 +
; + })`) + ).toMatchInlineSnapshot(/*js*/ ` + "const App = Component(() => { + const Branch_1 = Component(() => { + return
1
; + }); + const Default_1 = Component(() => { + return
+ 1}>{count} is bigger than is 1 + {count} is smaller than 1 +
; + }); + return <> 1}>; + });" + `); + }); + + it('should work with multi if', () => { + expect( + mock(/*js*/ ` + const App = Component(() => { + if (count > 1) { + return
1
+ } + if (count > 2) { + return
2
+ } + return
; + }) + `) + ).toMatchInlineSnapshot(/*js*/ ` + "const App = Component(() => { + const Branch_1 = Component(() => { + return
1
; + }); + const Default_1 = Component(() => { + const Branch_2 = Component(() => { + return
2
; + }); + const Default_2 = Component(() => { + return
; + }); + return <> 2}>; + }); + return <> 1}>; + });" + `); + }); + + it('should work with nested if', () => { + expect( + mock(/*js*/ ` + const App = Component(() => { + if (count > 1) { + if (count > 2) { + return
2
+ } + return
1
+ } + return
; + }) + `) + ).toMatchInlineSnapshot(/*js*/ ` + "const App = Component(() => { + const Branch_1 = Component(() => { + const Branch_2 = Component(() => { + return
2
; + }); + const Default_2 = Component(() => { + return
1
; + }); + return <> 2}>; + }); + const Default_1 = Component(() => { + return
; + }); + return <> 1}>; + });" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/earlyReturnAnanlyze.test.x.ts b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/earlyReturnAnanlyze.test.x.ts deleted file mode 100644 index 7ebfb786..00000000 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/earlyReturnAnanlyze.test.x.ts +++ /dev/null @@ -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
1
- } - return
- 1}>{count} is bigger than is 1 - {count} is smaller than 1 -
; - } - `); - 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
1
- } - if (count > 2) { - return
2
- } - return
; - } - `); - 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
2
- } - return
1
- } - return
; - } - `); - 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); - }); -}); diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.ts b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.ts new file mode 100644 index 00000000..220f4533 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.ts @@ -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 =
+ } + `) + ).toMatchInlineSnapshot(/*jsx*/ ` + "function App() { + const a = Component(() =>
); + }" + `); + }); + + it('should work with jsx slice in ternary operator', () => { + expect( + mock(` + function App() { + const a = true ? :
+ } + `) + ).toMatchInlineSnapshot(` + "function App() { + const JSX_Table_Col = Component(() => ); + const JSX_div = Component(() =>
); + const a = true ? : ; + }" + `); + }); + + it('should work with jsx slice in arr', () => { + expect( + mock(` + function App() { + const arr = [
,

] + } + `) + ).toMatchInlineSnapshot(` + "function App() { + const JSX_div = Component(() =>
); + const JSX_h = Component(() =>

); + const arr = [, ]; + }" + `); + }); + + it('fragment should work', () => { + expect( + mock(` + function App() { + const a = <>{test} + const b = cond ? <>
: <> + } + `) + ).toMatchInlineSnapshot(` + "function App() { + const a = Component(() => <>{test}); + const JSX_Fragment = Component(() => <>
); + const JSX_Fragment2 = Component(() => <>); + const b = cond ? : ; + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.x.ts b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.x.ts deleted file mode 100644 index 45d4fa5e..00000000 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.x.ts +++ /dev/null @@ -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 =
- } - `); - - 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 ?
:

- } - `); - - expect(root.state[0].name).toBe('a'); - expect(root.subComponents[0].name).toBe('$$div-Sub0'); - expect(genCode(root.subComponents[0].child)).toMatchInlineSnapshot(`"
"`); - expect(root.subComponents[1].name).toBe('$$h1-Sub1'); - expect(genCode(root.subComponents[1].child)).toMatchInlineSnapshot(`"

"`); - 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 = [
,

] - } - `); - - expect(root.state[0].name).toBe('a'); - expect(root.subComponents[0].name).toBe('$$div-Sub0'); - expect(genCode(root.subComponents[0].child)).toMatchInlineSnapshot(`"
"`); - expect(root.subComponents[1].name).toBe('$$h1-Sub1'); - expect(genCode(root.subComponents[1].child)).toMatchInlineSnapshot(`"

"`); - expect(genCode(root.state[0].value)).toMatchInlineSnapshot(`"true ? <$$div-Sub0 /> : <$$h1-Sub1 />"`); - }); - }); -}); diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/mock.ts b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/mock.ts new file mode 100644 index 00000000..9e6793b7 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/mock.ts @@ -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; +} diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/props.test.ts b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/props.test.ts new file mode 100644 index 00000000..83c75b08 --- /dev/null +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/props.test.ts @@ -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; + });" + `); + }); + }); +});