diff --git a/packages/inula-next-shared/package.json b/packages/inula-next-shared/package.json new file mode 100644 index 0000000000000000000000000000000000000000..a81c26c9850416615d6ea98e780561fb01fe12a8 --- /dev/null +++ b/packages/inula-next-shared/package.json @@ -0,0 +1,33 @@ +{ + "name": "@openinula/next-shared", + "version": "0.0.1", + "description": "Inula Next Shared", + "keywords": [ + "Inula-Next" + ], + "license": "MIT", + "files": [ + "src" + ], + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsup --sourcemap" + }, + "devDependencies": { + "typescript": "^5.3.2", + "tsup": "^8.2.4" + }, + "tsup": { + "entry": [ + "src/index.ts" + ], + "format": [ + "cjs", + "esm" + ], + "clean": true, + "dts": true, + "sourceMap": true + } +} diff --git a/packages/inula-next-shared/src/index.ts b/packages/inula-next-shared/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..798bdbbcf76fb9cc4ab485049a1285340d231a64 --- /dev/null +++ b/packages/inula-next-shared/src/index.ts @@ -0,0 +1,13 @@ +export enum InulaNodeType { + Comp = 0, + For = 1, + Cond = 2, + Exp = 3, + Hook = 4, + Context = 5, + Children = 6, +} + +export function getTypeName(type: InulaNodeType): string { + return InulaNodeType[type]; +} diff --git a/packages/inula-next-shared/tsconfig.json b/packages/inula-next-shared/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..c9c265f5fbe6bf7f65abe1dc04a7bf03892f6a0f --- /dev/null +++ b/packages/inula-next-shared/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true + }, + "ts-node": { + "esm": true + } +} + diff --git a/packages/inula-next/README.md b/packages/inula-next/README.md index 6dfb435b453ba2cfe0a1f218284179934ff8a6c5..b23a459bb6f6d8d11712fc89b2413aa5221534cb 100644 --- a/packages/inula-next/README.md +++ b/packages/inula-next/README.md @@ -1,2 +1,2 @@ -# Inula-Next Main Package -See the website's documentations for usage. +# Inula-Next Main Package +See the website's documentations for usage. diff --git a/packages/inula-next/package.json b/packages/inula-next/package.json index 1f31429fcd9151e48fc1ed2baab24ed06c7a312e..b0b17f3cd3d79089acb07b44c509e7a05592d958 100644 --- a/packages/inula-next/package.json +++ b/packages/inula-next/package.json @@ -1,40 +1,41 @@ -{ - "name": "@openinula/next", - "version": "0.0.2", - "keywords": [ - "inula" - ], - "license": "MIT", - "files": [ - "dist", - "README.md" - ], - "type": "module", - "main": "dist/index.cjs", - "module": "dist/index.js", - "typings": "dist/index.d.ts", - "scripts": { - "build": "tsup --sourcemap && cp src/index.d.ts dist/ && cp -r src/types dist/", - "test": "vitest --ui" - }, - "dependencies": { - "csstype": "^3.1.3", - "@openinula/store": "workspace:*" - }, - "devDependencies": { - "tsup": "^6.5.0", - "@openinula/vite-plugin-inula-next": "workspace:*", - "vitest": "^1.2.2" - }, - "tsup": { - "entry": [ - "src/index.js" - ], - "format": [ - "cjs", - "esm" - ], - "clean": true, - "minify": true - } -} +{ + "name": "@openinula/next", + "version": "0.0.2", + "keywords": [ + "inula" + ], + "license": "MIT", + "files": [ + "dist", + "README.md" + ], + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "typings": "dist/index.d.ts", + "scripts": { + "build": "tsup --sourcemap", + "test": "vitest --ui" + }, + "dependencies": { + "csstype": "^3.1.3", + "@openinula/next-shared": "workspace:*" + }, + "devDependencies": { + "tsup": "^6.5.0", + "@openinula/vite-plugin-inula-next": "workspace:*", + "vitest": "2.0.5" + }, + "tsup": { + "entry": [ + "src/index.ts" + ], + "format": [ + "cjs", + "esm" + ], + "clean": true, + "minify": false, + "noExternal": ["@openinula/next-shared"] + } +} diff --git a/packages/inula-next/src/ChildrenNode.ts b/packages/inula-next/src/ChildrenNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..ad04d88d7d12af17a3825198c2804935fe28a668 --- /dev/null +++ b/packages/inula-next/src/ChildrenNode.ts @@ -0,0 +1,41 @@ +import { addWillUnmount } from './lifecycle'; +import { ChildrenNode, VNode, Updater } from './types'; +import { InulaNodeType } from '@openinula/next-shared'; + +export function createChildrenNode(childrenFunc: (addUpdate: (updater: Updater) => void) => VNode[]): ChildrenNode { + return { + __type: InulaNodeType.Children, + childrenFunc, + updaters: new Set(), + }; +} + +/** + * @brief Build the prop view by calling the childrenFunc and add every single instance of the returned InulaNode to updaters + * @returns An array of InulaNode instances returned by childrenFunc + */ +export function buildChildren(childrenNode: ChildrenNode) { + let update; + const addUpdate = (updateFunc: Updater) => { + update = updateFunc; + childrenNode.updaters.add(updateFunc); + }; + const newNodes = childrenNode.childrenFunc(addUpdate); + if (newNodes.length === 0) return []; + if (update) { + // Remove the updateNode from dlUpdateNodes when it unmounts + addWillUnmount(newNodes[0], childrenNode.updaters.delete.bind(childrenNode.updaters, update)); + } + + return newNodes; +} + +/** + * @brief Update every node in dlUpdateNodes + * @param changed - A parameter indicating what changed to trigger the update + */ +export function updateChildrenNode(childrenNode: ChildrenNode, changed: number) { + childrenNode.updaters.forEach(update => { + update(changed); + }); +} diff --git a/packages/inula-next/src/CompNode.js b/packages/inula-next/src/CompNode.js deleted file mode 100644 index 5368589e3ee5d91c8ca57cda387d2b488252c31e..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/CompNode.js +++ /dev/null @@ -1,264 +0,0 @@ -import { DLNode, DLNodeType } from './DLNode'; -import { forwardHTMLProp } from './HTMLNode'; -import { cached } from './store'; -import { schedule } from './scheduler'; -import { inMount } from './index.js'; - -/** - * @class - * @extends import('./DLNode').DLNode - */ -export class CompNode extends DLNode { - /** - * @brief Constructor, Comp type - * @internal - * * key - private property key - * * $$key - dependency number, e.g. 0b1, 0b10, 0b100 - * * $s$key - set of properties that depend on this property - * * $p$key - exist if this property is a prop - * * $e$key - exist if this property is an env - * * $en$key - exist if this property is an env, and it's the innermost env that contains this env - * * $w$key - exist if this property is a watcher - * * $f$key - a function that returns the value of this property, called when the property's dependencies change - * * _$children - children nodes of type PropView - * * _$contentKey - the key key of the content prop - * * _$forwardProps - exist if this node is forwarding props - * * _$forwardPropsId - the keys of the props that this node is forwarding, collected in _$setForwardProp - * * _$forwardPropsSet - contain all the nodes that are forwarding props to this node, collected with _$addForwardProps - */ - constructor() { - super(DLNodeType.Comp); - } - - setUpdateFunc({ updateState, updateProp, updateContext, getUpdateViews, didUnmount, willUnmount, didMount }) { - this.updateState = updateState; - this._$updateProp = updateProp; - if (updateContext) this.updateContext = updateContext; - this.getUpdateViews = getUpdateViews; - this.didUnmount = didUnmount; - this.willUnmount = willUnmount; - this.didMount = didMount; - } - - updateProp(...args) { - this._$updateProp(...args); - } - - /** - * @brief Init function, called explicitly in the subclass's constructor - */ - init() { - this._$notInitd = true; - - const willCall = () => { - this._$callUpdatesBeforeInit(); - this.didMount && DLNode.addDidMount(this, this.didMount.bind(this)); - this.willUnmount && DLNode.addWillUnmount(this, this.willUnmount.bind(this)); - DLNode.addDidUnmount(this, this._$setUnmounted.bind(this)); - this.didUnmount && DLNode.addDidUnmount(this, this.didUnmount.bind(this)); - if (this.getUpdateViews) { - const result = this.getUpdateViews(); - // TODO: Need refactor - if (Array.isArray(result)) { - const [baseNode, updateView] = result; - this.updateView = updateView; - this._$nodes = baseNode; - } else { - this.updateView = result; - } - } - }; - - if (this._$catchable) { - this._$catchable(willCall)(); - if (this._$update) this._$update = this._$catchable(this._$update.bind(this)); - this.updateDerived = this._$catchable(this.updateDerived.bind(this)); - delete this._$catchable; - } else { - willCall(); - } - - return this; - } - - _$setUnmounted() { - this._$unmounted = true; - } - - /** - * @brief Call updates manually before the node is mounted - */ - _$callUpdatesBeforeInit() { - // invoke full updateState - this.updateState(-1); - delete this._$notInitd; - } - - /** - * @brief Set all the props to forward - * @param key - * @param value - * @param deps - */ - _$setPropToForward(key, value, deps) { - this._$forwardPropsSet.forEach(node => { - if (node._$dlNodeType === DLNodeType.Comp) { - node._$setProp(key, () => value, deps); - return; - } - if (node instanceof HTMLElement) { - forwardHTMLProp(node, key, () => value, deps); - } - }); - } - - /** - * @brief Define forward props - * @param key - * @param value - */ - _$setForwardProp(key, valueFunc, deps) { - const notInitd = '_$notInitd' in this; - if (!notInitd && this._$cache(key, deps)) return; - const value = valueFunc(); - if (key === '_$content' && this._$contentKey) { - this[this._$contentKey] = value; - this.updateDerived(this._$contentKey); - } - this[key] = value; - this.updateDerived(key); - if (notInitd) this._$forwardPropsId.push(key); - else this._$setPropToForward(key, value, deps); - } - - /** - * @brief Cache the deps and return true if the deps are the same as the previous deps - * @param key - * @param deps - * @returns - */ - _$cache(key, deps) { - if (!deps || !deps.length) return false; - const cacheKey = `$cc$${key}`; - if (cached(deps, this[cacheKey])) return true; - this[cacheKey] = deps; - return false; - } - - /** - * @brief Set the content prop, the key is stored in _$contentKey - * @param value - */ - _$setContent(valueFunc, deps) { - if ('_$forwardProps' in this) return this._$setForwardProp('_$content', valueFunc, deps); - const contentKey = this._$contentKey; - if (!contentKey) return; - if (this._$cache(contentKey, deps)) return; - this[contentKey] = valueFunc(); - this.updateDerived(contentKey); - } - - /** - * @brief Set a prop directly, if this is a forwarded prop, go and init forwarded props - * @param key - * @param valueFunc - * @param deps - */ - _$setProp(key, valueFunc, deps) { - if (this._$cache(key, deps)) return; - if (key === '*spread*') { - const spread = valueFunc(); - Object.keys(spread).forEach(key => { - this.updateProp(key, this[key]); - }); - return; - } - this[key] = valueFunc(); - this.updateProp(key, this[key]); - } - - _$setProps(valueFunc, deps) { - if (this._$cache('props', deps)) return; - const props = valueFunc(); - if (!props) return; - Object.entries(props).forEach(([key, value]) => { - this._$setProp(key, () => value, []); - }); - } - - // ---- Update functions - /** - * @brief Update an env, called in EnvNode._$update - * @param key - * @param value - * @param context - */ - _$updateContext(key, value, context) { - if (!this.updateContext) return; - this.updateContext(context, key, value); - } - - /** - * @brief Update a prop - */ - _$ud(exp, key) { - this.updateDerived(key); - return exp; - } - - /** - * @brief Update properties that depend on this property - * @param {any} newValue - * @param {number} bit - */ - updateDerived(newValue, bit) { - if ('_$notInitd' in this) return; - - // ---- Update all properties that depend on this property - this.updateState(bit); - - // ---- "trigger-view" - if (!inMount()) { - this._$updateView(bit); - } - } - - /** - * - * @param {number} bit - * @private - */ - _$updateView(bit) { - // if (!('_$update' in this)) return; - if (!bit) return; - // ---- Collect all depNums that need to be updated - if ('_$depNumsToUpdate' in this) { - this._$depNumsToUpdate.push(bit); - } else { - this._$depNumsToUpdate = [bit]; - // ---- Update in the next microtask - schedule(() => { - // ---- Abort if unmounted - if (this._$unmounted) return; - const depNums = this._$depNumsToUpdate; - if (depNums.length > 0) { - const depNum = depNums.reduce((acc, cur) => acc | cur, 0); - this.updateView(depNum); - } - delete this._$depNumsToUpdate; - }); - } - } -} - -// ---- @View -> class Comp extends View -export const View = CompNode; - -/** - * @brief Run all update functions given the key - * @param dlNode - * @param key - */ -export function update(dlNode, key) { - dlNode.updateDerived(key); -} diff --git a/packages/inula-next/src/CompNode.ts b/packages/inula-next/src/CompNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2636ee9b96536100afd62fcbb8829cefa368e4f --- /dev/null +++ b/packages/inula-next/src/CompNode.ts @@ -0,0 +1,138 @@ +import { addDidMount, addDidUnmount, addWillUnmount } from './lifecycle'; +import { equal } from './equal'; +import { schedule } from './scheduler'; +import { inMount } from './index'; +import { CompNode, ComposableNode } from './types'; +import { InulaNodeType } from '@openinula/next-shared'; + +export function createCompNode(): CompNode { + return { + updateProp: builtinUpdateFunc, + updateState: builtinUpdateFunc, + __type: InulaNodeType.Comp, + props: {}, + _$nodes: [], + }; +} + +export function builtinUpdateFunc() { + throw new Error('Component node not initiated.'); +} + +export function constructComp( + comp: CompNode, + { + updateState, + updateProp, + updateContext, + getUpdateViews, + didUnmount, + willUnmount, + didMount, + }: Pick< + CompNode, + 'updateState' | 'updateProp' | 'updateContext' | 'getUpdateViews' | 'didUnmount' | 'willUnmount' | 'didMount' + > +): CompNode { + comp.updateState = updateState; + comp.updateProp = updateProp; + comp.updateContext = updateContext; + comp.getUpdateViews = getUpdateViews; + comp.didUnmount = didUnmount; + comp.willUnmount = willUnmount; + comp.didMount = didMount; + + return comp; +} + +export function initCompNode(node: CompNode): CompNode { + node.mounting = true; + const willCall = () => { + callUpdatesBeforeInit(node); + if (node.didMount) addDidMount(node, node.didMount); + if (node.willUnmount) addWillUnmount(node, node.willUnmount); + addDidUnmount(node, setUnmounted.bind(null, node)); + if (node.didUnmount) addDidUnmount(node, node.didUnmount); + if (node.getUpdateViews) { + const result = node.getUpdateViews(); + if (Array.isArray(result)) { + const [baseNode, updateView] = result; + node.updateView = updateView; + node._$nodes = baseNode; + } else { + node.updateView = result; + } + } + }; + + willCall(); + + return node; +} + +function setUnmounted(node: CompNode) { + node._$unmounted = true; +} + +function callUpdatesBeforeInit(node: CompNode) { + node.updateState(-1); + delete node.mounting; +} + +function cacheCheck(node: CompNode, key: string, deps: any[]): boolean { + if (!deps || !deps.length) return false; + if (!node.cache) { + node.cache = {}; + } + if (equal(deps, node.cache[key])) return true; + node.props[key] = deps; + return false; +} + +export function setProp(node: CompNode, key: string, valueFunc: () => any, deps: any[]) { + if (cacheCheck(node, key, deps)) return; + node.props[key] = valueFunc(); + node.updateProp(key, node.props[key]); +} + +export function setProps(node: CompNode, valueFunc: () => Record, deps: any[]) { + if (cacheCheck(node, 'props', deps)) return; + const props = valueFunc(); + if (!props) return; + Object.entries(props).forEach(([key, value]) => { + setProp(node, key, () => value, []); + }); +} + +export function updateContext(node: CompNode, key: string, value: any, context: any) { + if (!node.updateContext) return; + node.updateContext(context, key, value); +} + +export function updateCompNode(node: ComposableNode, newValue: any, bit?: number) { + if ('mounting' in node) return; + + node.updateState(bit || 0); + + if (!inMount()) { + updateView(node, bit || 0); + } +} + +function updateView(node: ComposableNode, bit: number) { + if (!bit) return; + if ('_$depNumsToUpdate' in node) { + node._$depNumsToUpdate?.push(bit); + } else { + node._$depNumsToUpdate = [bit]; + schedule(() => { + if (node._$unmounted) return; + const depNums = node._$depNumsToUpdate || []; + if (depNums.length > 0) { + const depNum = depNums.reduce((acc, cur) => acc | cur, 0); + node.updateView?.(depNum); + } + delete node._$depNumsToUpdate; + }); + } +} diff --git a/packages/inula-next/src/ContextNode.ts b/packages/inula-next/src/ContextNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef10dedf4543bd9b4800366028437e5054348cc4 --- /dev/null +++ b/packages/inula-next/src/ContextNode.ts @@ -0,0 +1,122 @@ +import { InulaNodeType } from '@openinula/next-shared'; +import { addWillUnmount } from './lifecycle'; +import { equal } from './equal'; +import { VNode, ContextNode, Context, CompNode, HookNode } from './types'; +import { currentComp } from '.'; + +let contextNodeMap: Map>; + +export function getContextNodeMap() { + return contextNodeMap; +} + +export function createContextNode>( + ctx: Context, + value: V, + depMap: Record> +) { + if (!contextNodeMap) contextNodeMap = new Map(); + + const ContextNode: ContextNode = { + value: value, + depMap: depMap, + context: ctx, + __type: InulaNodeType.Context, + consumers: new Set(), + _$nodes: [], + }; + + replaceContextValue(ContextNode); + + return ContextNode; +} + +/** + * @brief Update a specific key of context, and update all the comp nodes that depend on this context + * @param contextNode + * @param name - The name of the environment variable to update + * @param valueFunc + * @param deps + */ +export function updateContextNode>( + contextNode: ContextNode, + name: keyof V, + valueFunc: () => V[keyof V], + deps: Array +) { + if (cached(contextNode, deps, name)) return; + const value = valueFunc(); + contextNode.value[name] = value; + contextNode.consumers.forEach(node => { + // should have updateContext, otherwise the bug of compiler + node.updateContext!(contextNode.context, name as string, value); + }); +} + +function cached>(contextNode: ContextNode, deps: Array, name: keyof V) { + if (!deps || !deps.length) return false; + if (equal(deps, contextNode.depMap[name])) return true; + contextNode.depMap[name] = deps; + return false; +} + +function replaceContextValue>(contextNode: ContextNode) { + contextNode.prevValue = contextNode.context.value; + contextNode.prevContextNode = contextNodeMap!.get(contextNode.context.id); + contextNode.context.value = contextNode.value; + + contextNodeMap!.set(contextNode.context.id, contextNode); +} + +/** + * @brief Set this._$nodes, and exit the current context + * @param contextNode + * @param nodes - The nodes to set + */ +export function initContextChildren>(contextNode: ContextNode, nodes: VNode[]) { + contextNode._$nodes = nodes; + contextNode.context.value = contextNode.prevValue || null; + if (contextNode.prevContextNode) { + contextNodeMap!.set(contextNode.context.id, contextNode.prevContextNode); + } else { + contextNodeMap!.delete(contextNode.context.id); + } + contextNode.prevValue = null; + contextNode.prevContextNode = null; +} + +export function replaceContext(contextNodeMap: Map>) { + for (const [ctxId, contextNode] of contextNodeMap.entries()) { + replaceContextValue(contextNode); + } +} + +/** + * @brief Add a node to this.updateNodes, delete the node from this.updateNodes when it unmounts + */ +export function addConsumer(contextNode: ContextNode, node: CompNode | HookNode) { + contextNode.consumers.add(node); + addWillUnmount(node, contextNode.consumers.delete.bind(contextNode.consumers, node)); +} + +export function createContext | null>(defaultVal: T): Context { + return { + id: Symbol('inula-ctx'), + value: defaultVal, + }; +} + +export function useContext | null>(ctx: Context, key?: keyof T): T | T[keyof T] { + if (contextNodeMap) { + const contextNode = contextNodeMap.get(ctx.id); + if (contextNode) { + addConsumer(contextNode, currentComp!); + } + } + + if (key && ctx.value) { + return ctx.value[key]; + } + + return ctx.value as T; +} diff --git a/packages/inula-next/src/ContextProvider.js b/packages/inula-next/src/ContextProvider.js deleted file mode 100644 index aefb17dc4480052614a1f0ae9110c058e9e3452d..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/ContextProvider.js +++ /dev/null @@ -1,77 +0,0 @@ -import { DLNode, DLNodeType } from './DLNode'; -import { DLStore, cached } from './store'; - -export class ContextProvider extends DLNode { - constructor(ctx, envs, depsArr) { - super(DLNodeType.Env); - // Declare a global variable to store the environment variables - if (!('DLEnvStore' in DLStore.global)) DLStore.global.envNodeMap = new Map(); - - this.context = ctx; - this.envs = envs; - this.depsArr = depsArr; - this.updateNodes = new Set(); - - this.replaceContextValue(); - } - - cached(deps, name) { - if (!deps || !deps.length) return false; - if (cached(deps, this.depsArr[name])) return true; - this.depsArr[name] = deps; - return false; - } - - /** - * @brief Update a specific env, and update all the comp nodes that depend on this env - * @param name - The name of the environment variable to update - * @param value - The new value of the environment variable - */ - updateContext(name, valueFunc, deps) { - if (this.cached(deps, name)) return; - const value = valueFunc(); - this.envs[name] = value; - this.updateNodes.forEach(node => { - node._$updateContext(name, value, this.context); - }); - } - - replaceContextValue() { - this.prevValue = this.context.value; - this.prevEnvNode = DLStore.global.envNodeMap.get(this.context.id); - this.context.value = this.envs; - - DLStore.global.envNodeMap.set(this.context.id, this); - } - - /** - * @brief Add a node to this.updateNodes, delete the node from this.updateNodes when it unmounts - * @param node - The node to add - */ - addNode(node) { - this.updateNodes.add(node); - DLNode.addWillUnmount(node, this.updateNodes.delete.bind(this.updateNodes, node)); - } - - /** - * @brief Set this._$nodes, and exit the current env - * @param nodes - The nodes to set - */ - initNodes(nodes) { - this._$nodes = nodes; - this.context.value = this.prevValue; - if (this.prevEnvNode) { - DLStore.global.envNodeMap.set(this.context.id, this.prevEnvNode); - } else { - DLStore.global.envNodeMap.delete(this.context.id); - } - this.prevValue = null; - this.prevEnvNode = null; - } -} - -export function replaceEnvNodes(envNodeMap) { - for (const [ctxId, envNode] of envNodeMap.entries()) { - envNode.replaceContextValue(); - } -} diff --git a/packages/inula-next/src/DLNode.js b/packages/inula-next/src/DLNode.js deleted file mode 100644 index d7b7fb8e494ca9137a436328c94104b3d3000e59..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/DLNode.js +++ /dev/null @@ -1,210 +0,0 @@ -import { DLStore } from './store'; - -export const DLNodeType = { - Comp: 0, - For: 1, - Cond: 2, - Env: 3, - Exp: 4, - Snippet: 5, - Try: 6, -}; - -export class DLNode { - /** - * @brief Node type: HTML, Text, Custom, For, If, Env, Expression - */ - _$dlNodeType; - - /** - * @brief Constructor - * @param nodeType - * @return {void} - */ - constructor(nodeType) { - this._$dlNodeType = nodeType; - } - - /** - * @brief Node element - * Either one real element for HTMLNode and TextNode - * Or an array of DLNode for CustomNode, ForNode, IfNode, EnvNode, ExpNode - */ - get _$el() { - return DLNode.toEls(this._$nodes); - } - - /** - * @brief Loop all child DLNodes to get all the child elements - * @param nodes - * @returns HTMLElement[] - */ - static toEls(nodes) { - const els = []; - this.loopShallowEls(nodes, el => { - els.push(el); - }); - return els; - } - - // ---- Loop nodes ---- - /** - * @brief Loop all elements shallowly, - * i.e., don't loop the child nodes of dom elements and only call runFunc on dom elements - * @param nodes - * @param runFunc - */ - static loopShallowEls(nodes, runFunc) { - const stack = [...nodes].reverse(); - while (stack.length > 0) { - const node = stack.pop(); - if (!('_$dlNodeType' in node)) runFunc(node); - else node._$nodes && stack.push(...[...node._$nodes].reverse()); - } - } - - /** - * @brief Add parentEl to all nodes until the first element - * @param nodes - * @param parentEl - */ - static addParentEl(nodes, parentEl) { - nodes.forEach(node => { - if ('_$dlNodeType' in node) { - node._$parentEl = parentEl; - node._$nodes && DLNode.addParentEl(node._$nodes, parentEl); - } - }); - } - - // ---- Flow index and add child elements ---- - /** - * @brief Get the total count of dom elements before the stop node - * @param nodes - * @param stopNode - * @returns total count of dom elements - */ - static getFlowIndexFromNodes(nodes, stopNode) { - let index = 0; - const stack = [...nodes].reverse(); - while (stack.length > 0) { - const node = stack.pop(); - if (node === stopNode) break; - if ('_$dlNodeType' in node) { - node._$nodes && stack.push(...[...node._$nodes].reverse()); - } else { - index++; - } - } - return index; - } - - /** - * @brief Given an array of nodes, append them to the parentEl - * 1. If nextSibling is provided, insert the nodes before the nextSibling - * 2. If nextSibling is not provided, append the nodes to the parentEl - * @param nodes - * @param parentEl - * @param nextSibling - * @returns Added element count - */ - static appendNodesWithSibling(nodes, parentEl, nextSibling) { - if (nextSibling) return this.insertNodesBefore(nodes, parentEl, nextSibling); - return this.appendNodes(nodes, parentEl); - } - - /** - * @brief Given an array of nodes, append them to the parentEl using the index - * 1. If the index is the same as the length of the parentEl.childNodes, append the nodes to the parentEl - * 2. If the index is not the same as the length of the parentEl.childNodes, insert the nodes before the node at the index - * @param nodes - * @param parentEl - * @param index - * @param length - * @returns Added element count - */ - static appendNodesWithIndex(nodes, parentEl, index, length) { - length = length ?? parentEl.childNodes.length; - if (length !== index) return this.insertNodesBefore(nodes, parentEl, parentEl.childNodes[index]); - return this.appendNodes(nodes, parentEl); - } - - /** - * @brief Insert nodes before the nextSibling - * @param nodes - * @param parentEl - * @param nextSibling - * @returns Added element count - */ - static insertNodesBefore(nodes, parentEl, nextSibling) { - let count = 0; - this.loopShallowEls(nodes, el => { - parentEl.insertBefore(el, nextSibling); - count++; - }); - return count; - } - - /** - * @brief Append nodes to the parentEl - * @param nodes - * @param parentEl - * @returns Added element count - */ - static appendNodes(nodes, parentEl) { - let count = 0; - this.loopShallowEls(nodes, el => { - parentEl.appendChild(el); - count++; - }); - return count; - } - - // ---- Lifecycle ---- - /** - * @brief Add willUnmount function to node - * @param node - * @param func - */ - static addWillUnmount(node, func) { - const willUnmountStore = DLStore.global.WillUnmountStore; - const currentStore = willUnmountStore[willUnmountStore.length - 1]; - // ---- If the current store is empty, it means this node is not mutable - if (!currentStore) return; - currentStore.push(func.bind(null, node)); - } - - /** - * @brief Add didUnmount function to node - * @param node - * @param func - */ - static addDidUnmount(node, func) { - const didUnmountStore = DLStore.global.DidUnmountStore; - const currentStore = didUnmountStore[didUnmountStore.length - 1]; - // ---- If the current store is empty, it means this node is not mutable - if (!currentStore) return; - currentStore.push(func.bind(null, node)); - } - - /** - * @brief Add didUnmount function to global store - * @param func - */ - static addDidMount(node, func) { - if (!DLStore.global.DidMountStore) DLStore.global.DidMountStore = []; - DLStore.global.DidMountStore.push(func.bind(null, node)); - } - - /** - * @brief Run all didMount functions and reset the global store - */ - static runDidMount() { - const didMountStore = DLStore.global.DidMountStore; - if (!didMountStore || didMountStore.length === 0) return; - for (let i = didMountStore.length - 1; i >= 0; i--) { - didMountStore[i](); - } - DLStore.global.DidMountStore = []; - } -} diff --git a/packages/inula-next/src/HookNode.js b/packages/inula-next/src/HookNode.js deleted file mode 100644 index 9514052b07d71b2fe77d44506766e46948903551..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/HookNode.js +++ /dev/null @@ -1,55 +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 { CompNode } from './CompNode.js'; -import { inMount } from './index.js'; - -export class HookNode extends CompNode { - /** - * - * @param {HookNode | CompNode} currentComp - * @param {number}bitMap - */ - constructor(currentComp, bitMap) { - super(); - this.parent = currentComp; - this.bitMap = bitMap; - } - - /** - * update prop - * @param {string} propName - * @param {any }value - */ - updateHook(propName, value) { - this.update(); - } - - emitUpdate() { - // the new value is not used in the `updateDerived`, just pass a null - this.parent.updateDerived(null, this.bitMap); - } - - setUpdateFunc({ value, ...updater }) { - super.setUpdateFunc(updater); - this.value = value; - } - - updateProp(...args) { - if (!inMount()) { - super.updateProp(...args); - } - } -} diff --git a/packages/inula-next/src/HookNode.ts b/packages/inula-next/src/HookNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6206a29e2cc00e31ba38da05faa0d3360dff913 --- /dev/null +++ b/packages/inula-next/src/HookNode.ts @@ -0,0 +1,70 @@ +/* + * 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 { builtinUpdateFunc, inMount, updateCompNode } from './index.js'; +import { CompNode, HookNode } from './types'; +import { InulaNodeType } from '@openinula/next-shared'; + +export function createHookNode(parent: HookNode | CompNode, bitmap: number): HookNode { + return { + updateProp: builtinUpdateFunc, + updateState: builtinUpdateFunc, + __type: InulaNodeType.Hook, + props: {}, + _$nodes: [], + bitmap, + parent, + }; +} + +export function emitUpdate(node: HookNode) { + // the new value is not used in the `updateCompNode`, just pass a null + updateCompNode(node.parent, null, node.bitmap); +} + +export function constructHook( + node: HookNode, + { + value, + updateState, + updateProp, + updateContext, + getUpdateViews, + didUnmount, + willUnmount, + didMount, + }: Pick< + HookNode, + | 'value' + | 'updateState' + | 'updateProp' + | 'updateContext' + | 'getUpdateViews' + | 'didUnmount' + | 'willUnmount' + | 'didMount' + > +): HookNode { + node.value = value; + node.updateState = updateState; + node.updateProp = updateProp; + node.updateContext = updateContext; + node.getUpdateViews = getUpdateViews; + node.didUnmount = didUnmount; + node.willUnmount = willUnmount; + node.didMount = didMount; + + return node; +} diff --git a/packages/inula-next/src/InulaNode.ts b/packages/inula-next/src/InulaNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..01b026e32e9064f0a4d092dd3a845dc6a2d418be --- /dev/null +++ b/packages/inula-next/src/InulaNode.ts @@ -0,0 +1,83 @@ +import { InulaHTMLNode, VNode, TextNode, InulaNode } from './types'; + +export const getEl = (node: VNode): Array => { + return toEls(node._$nodes || []); +}; + +export const toEls = (nodes: InulaNode[]): Array => { + const els: Array = []; + loopShallowEls(nodes, el => { + els.push(el); + }); + return els; +}; + +export const loopShallowEls = (nodes: InulaNode[], runFunc: (el: InulaHTMLNode | TextNode) => void): void => { + const stack: Array = [...nodes].reverse(); + while (stack.length > 0) { + const node = stack.pop()!; + if (node instanceof HTMLElement || node instanceof Text) { + runFunc(node); + } else if (node._$nodes) { + stack.push(...[...node._$nodes].reverse()); + } + } +}; + +export const addParentEl = (nodes: Array, parentEl: HTMLElement): void => { + nodes.forEach(node => { + if ('__type' in node) { + node._$parentEl = parentEl as InulaHTMLNode; + node._$nodes && addParentEl(node._$nodes, parentEl); + } + }); +}; + +export const getFlowIndexFromNodes = (nodes: InulaNode[], stopNode?: InulaNode): number => { + let index = 0; + const stack: InulaNode[] = [...nodes].reverse(); + while (stack.length > 0) { + const node = stack.pop()!; + if (node === stopNode) break; + if ('__type' in node) { + node._$nodes && stack.push(...[...node._$nodes].reverse()); + } else { + index++; + } + } + return index; +}; + +export const appendNodesWithSibling = (nodes: Array, parentEl: HTMLElement, nextSibling?: Node): number => { + if (nextSibling) return insertNodesBefore(nodes, parentEl, nextSibling); + return appendNodes(nodes, parentEl); +}; + +export const appendNodesWithIndex = ( + nodes: InulaNode[], + parentEl: HTMLElement, + index: number, + length?: number +): number => { + length = length ?? parentEl.childNodes.length; + if (length !== index) return insertNodesBefore(nodes, parentEl, parentEl.childNodes[index]); + return appendNodes(nodes, parentEl); +}; + +export const insertNodesBefore = (nodes: InulaNode[], parentEl: HTMLElement, nextSibling: Node): number => { + let count = 0; + loopShallowEls(nodes, el => { + parentEl.insertBefore(el, nextSibling); + count++; + }); + return count; +}; + +const appendNodes = (nodes: InulaNode[], parentEl: HTMLElement): number => { + let count = 0; + loopShallowEls(nodes, el => { + parentEl.appendChild(el); + count++; + }); + return count; +}; diff --git a/packages/inula-next/src/MutableNode/CondNode.js b/packages/inula-next/src/MutableNode/CondNode.js deleted file mode 100644 index dbf75f4891a51d12129d3fd469ae7203f67b5fc6..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/MutableNode/CondNode.js +++ /dev/null @@ -1,69 +0,0 @@ -import { DLNodeType } from '../DLNode'; -import { FlatNode } from './FlatNode'; - -export class CondNode extends FlatNode { - /** - * @brief Constructor, If type, accept a function that returns a list of nodes - * @param caseFunc - */ - constructor(depNum, condFunc) { - super(DLNodeType.Cond); - this.depNum = depNum; - this.cond = -1; - this.condFunc = condFunc; - this.initUnmountStore(); - this._$nodes = this.condFunc(this); - this.setUnmountFuncs(); - - // ---- Add to the global UnmountStore - CondNode.addWillUnmount(this, this.runWillUnmount.bind(this)); - CondNode.addDidUnmount(this, this.runDidUnmount.bind(this)); - } - - /** - * @brief Update the nodes in the environment - */ - updateCond(key) { - // ---- Need to save prev unmount funcs because we can't put removeNodes before geneNewNodesInEnv - // The reason is that if it didn't change, we don't need to unmount or remove the nodes - const prevFuncs = [this.willUnmountFuncs, this.didUnmountFuncs]; - const newNodes = this.geneNewNodesInEnv(() => this.condFunc(this)); - - // ---- If the new nodes are the same as the old nodes, we only need to update children - if (this.didntChange) { - [this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs; - this.didntChange = false; - this.updateFunc?.(this.depNum, key); - return; - } - // ---- Remove old nodes - const newFuncs = [this.willUnmountFuncs, this.didUnmountFuncs]; - [this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs; - this._$nodes && this._$nodes.length > 0 && this.removeNodes(this._$nodes); - [this.willUnmountFuncs, this.didUnmountFuncs] = newFuncs; - - if (newNodes.length === 0) { - // ---- No branch has been taken - this._$nodes = []; - return; - } - // ---- Add new nodes - const parentEl = this._$parentEl; - // ---- Faster append with nextSibling rather than flowIndex - const flowIndex = CondNode.getFlowIndexFromNodes(parentEl._$nodes, this); - - const nextSibling = parentEl.childNodes[flowIndex]; - CondNode.appendNodesWithSibling(newNodes, parentEl, nextSibling); - CondNode.runDidMount(); - this._$nodes = newNodes; - } - - /** - * @brief The update function of IfNode's childNodes is stored in the first child node - * @param changed - */ - update(changed) { - if (!(~this.depNum & changed)) return; - this.updateFunc?.(changed); - } -} diff --git a/packages/inula-next/src/MutableNode/CondNode.ts b/packages/inula-next/src/MutableNode/CondNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..932bdf67fb6fab4e5c092cc1576006693f7baa90 --- /dev/null +++ b/packages/inula-next/src/MutableNode/CondNode.ts @@ -0,0 +1,91 @@ +import { appendNodesWithSibling, getFlowIndexFromNodes } from '../InulaNode.js'; +import { addDidUnmount, addWillUnmount, runDidMount } from '../lifecycle.js'; +import { + geneNewNodesInEnvWithUnmount, + removeNodesWithUnmount, + runLifeCycle, + setUnmountFuncs, +} from './mutableHandler.js'; +import { CondNode, VNode } from '../types'; +import { geneNewNodesInCtx, getSavedCtxNodes, removeNodes } from './mutableHandler.js'; +import { startUnmountScope } from '../lifecycle.js'; +import { InulaNodeType } from '@openinula/next-shared'; + +export function createCondNode(depNum: number, condFunc: (condNode: CondNode) => VNode[]) { + startUnmountScope(); + + const condNode: CondNode = { + __type: InulaNodeType.Cond, + cond: -1, + didntChange: false, + depNum, + condFunc, + savedContextNodes: getSavedCtxNodes(), + _$nodes: [], + willUnmountFuncs: [], + didUnmountFuncs: [], + }; + + condNode._$nodes = condFunc(condNode); + setUnmountFuncs(condNode); + + if (condNode.willUnmountFuncs) { + // ---- Add condNode willUnmount func to the global UnmountStore + addWillUnmount(condNode, runLifeCycle.bind(condNode, condNode.willUnmountFuncs)); + } + if (condNode.didUnmountFuncs) { + // ---- Add condNode didUnmount func to the global UnmountStore + addDidUnmount(condNode, runLifeCycle.bind(condNode, condNode.didUnmountFuncs)); + } + + return condNode; +} + +/** + * @brief the condition changed, update children of current branch + */ +export function updateCondChildren(condNode: CondNode, changed: number) { + if (condNode.depNum & changed) { + // If the depNum of the condition has changed, directly return because node already updated in the `updateBranch` + return; + } + condNode.updateFunc?.(changed); +} + +/** + * @brief The update function of CondNode's childNodes when the condition changed + */ +export function updateCondNode(condNode: CondNode) { + // ---- Need to save prev unmount funcs because we can't put removeNodes before geneNewNodesInEnv + // The reason is that if it didn't change, we don't need to unmount or remove the nodes + const prevFuncs = [condNode.willUnmountFuncs, condNode.didUnmountFuncs]; + const newNodes = geneNewNodesInEnvWithUnmount(condNode, () => condNode.condFunc(condNode)); + + // ---- If the new nodes are the same as the old nodes, we only need to update children + if (condNode.didntChange) { + [condNode.willUnmountFuncs, condNode.didUnmountFuncs] = prevFuncs; + condNode.didntChange = false; + condNode.updateFunc?.(condNode.depNum); + return; + } + // ---- Remove old nodes + const newFuncs = [condNode.willUnmountFuncs, condNode.didUnmountFuncs]; + [condNode.willUnmountFuncs, condNode.didUnmountFuncs] = prevFuncs; + condNode._$nodes && condNode._$nodes.length > 0 && removeNodesWithUnmount(condNode, condNode._$nodes); + [condNode.willUnmountFuncs, condNode.didUnmountFuncs] = newFuncs; + + if (newNodes.length === 0) { + // ---- No branch has been taken + condNode._$nodes = []; + return; + } + // ---- Add new nodes + const parentEl = condNode._$parentEl!; + // ---- Faster append with nextSibling rather than flowIndex + const flowIndex = getFlowIndexFromNodes(parentEl._$nodes, condNode); + + const nextSibling = parentEl.childNodes[flowIndex]; + appendNodesWithSibling(newNodes, parentEl, nextSibling); + runDidMount(); + condNode._$nodes = newNodes; +} diff --git a/packages/inula-next/src/MutableNode/ExpNode.js b/packages/inula-next/src/MutableNode/ExpNode.js deleted file mode 100644 index 0373dc1f66e8bb83b936c0b7a7a16e95f36e14cf..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/MutableNode/ExpNode.js +++ /dev/null @@ -1,86 +0,0 @@ -import { DLNodeType } from '../DLNode'; -import { FlatNode } from './FlatNode'; -import { DLStore, cached } from '../store'; - -export class ExpNode extends FlatNode { - /** - * @brief Constructor, Exp type, accept a function that returns a list of nodes - * @param nodesFunc - */ - constructor(value, deps) { - super(DLNodeType.Exp); - this.initUnmountStore(); - this._$nodes = ExpNode.formatNodes(value); - this.setUnmountFuncs(); - this.deps = this.parseDeps(deps); - // ---- Add to the global UnmountStore - ExpNode.addWillUnmount(this, this.runWillUnmount.bind(this)); - ExpNode.addDidUnmount(this, this.runDidUnmount.bind(this)); - } - - parseDeps(deps) { - return deps.map(dep => { - // ---- CompNode - if (dep?.prototype?._$init) return dep.toString(); - // ---- SnippetNode - if (dep?.propViewFunc) return dep.propViewFunc.toString(); - return dep; - }); - } - - cache(deps) { - if (!deps || !deps.length) return false; - deps = this.parseDeps(deps); - if (cached(deps, this.deps)) return true; - this.deps = deps; - return false; - } - /** - * @brief Generate new nodes and replace the old nodes - */ - update(valueFunc, deps) { - if (this.cache(deps)) return; - this.removeNodes(this._$nodes); - const newNodes = this.geneNewNodesInEnv(() => ExpNode.formatNodes(valueFunc())); - if (newNodes.length === 0) { - this._$nodes = []; - return; - } - - // ---- Add new nodes - const parentEl = this._$parentEl; - const flowIndex = ExpNode.getFlowIndexFromNodes(parentEl._$nodes, this); - const nextSibling = parentEl.childNodes[flowIndex]; - ExpNode.appendNodesWithSibling(newNodes, parentEl, nextSibling); - ExpNode.runDidMount(); - - this._$nodes = newNodes; - } - - /** - * @brief Format the nodes - * @param nodes - * @returns New nodes - */ - static formatNodes(nodes) { - if (!Array.isArray(nodes)) nodes = [nodes]; - return ( - nodes - // ---- Flatten the nodes - .flat(1) - // ---- Filter out empty nodes - .filter(node => node !== undefined && node !== null && typeof node !== 'boolean') - .map(node => { - // ---- If the node is a string, number or bigint, convert it to a text node - if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') { - return DLStore.document.createTextNode(`${node}`); - } - // ---- If the node has PropView, call it to get the view - if ('propViewFunc' in node) return node.build(); - return node; - }) - // ---- Flatten the nodes again - .flat(1) - ); - } -} diff --git a/packages/inula-next/src/MutableNode/ExpNode.ts b/packages/inula-next/src/MutableNode/ExpNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..6238d23e31c72f7bed9e043d4d19364159056fb7 --- /dev/null +++ b/packages/inula-next/src/MutableNode/ExpNode.ts @@ -0,0 +1,84 @@ +import { InulaNodeType } from '@openinula/next-shared'; +import { appendNodesWithSibling, getFlowIndexFromNodes } from '../InulaNode'; +import { addDidUnmount, addWillUnmount, runDidMount } from '../lifecycle'; +import { equal } from '../equal'; +import { ChildrenNode, ExpNode, VNode, TextNode, InulaNode } from '../types'; +import { removeNodesWithUnmount, runLifeCycle, setUnmountFuncs } from './mutableHandler'; +import { geneNewNodesInCtx, getSavedCtxNodes, removeNodes } from './mutableHandler'; +import { startUnmountScope } from '../lifecycle'; +import { buildChildren } from '../ChildrenNode'; +import { createTextNode } from '../renderer/dom'; + +function isChildrenNode(node: any): node is ChildrenNode { + return node.__type === InulaNodeType.Children; +} +function getExpressionResult(fn: () => Array) { + let nodes = fn(); + if (!Array.isArray(nodes)) nodes = [nodes]; + return ( + nodes + // ---- Flatten the nodes + .flat(1) + // ---- Filter out empty nodes + .filter(node => node !== undefined && node !== null && typeof node !== 'boolean') + .map(node => { + // ---- If the node is a string, number or bigint, convert it to a text node + if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') { + return createTextNode(`${node}`); + } + // TODO ---- If the node has PropView, call it to get the view, + if (isChildrenNode(node)) return buildChildren(node); + return node; + }) + // ---- Flatten the nodes again + .flat(1) + ); +} +export function createExpNode(value: () => VNode[], deps: unknown[]) { + startUnmountScope(); + + const expNode: ExpNode = { + __type: InulaNodeType.Exp, + _$nodes: getExpressionResult(value), + deps, + savedContextNodes: getSavedCtxNodes(), + willUnmountFuncs: [], + didUnmountFuncs: [], + }; + + setUnmountFuncs(expNode); + + if (expNode.willUnmountFuncs) { + // ---- Add expNode willUnmount func to the global UnmountStore + addWillUnmount(expNode, runLifeCycle.bind(expNode, expNode.willUnmountFuncs)); + } + if (expNode.didUnmountFuncs) { + // ---- Add expNode didUnmount func to the global UnmountStore + addDidUnmount(expNode, runLifeCycle.bind(expNode, expNode.didUnmountFuncs)); + } + return expNode; +} + +export function updateExpNode(expNode: ExpNode, valueFunc: () => VNode[], deps: unknown[]) { + if (cache(expNode, deps)) return; + removeNodesWithUnmount(expNode, expNode._$nodes); + const newNodes = geneNewNodesInCtx(expNode, () => getExpressionResult(valueFunc)); + if (newNodes.length === 0) { + expNode._$nodes = []; + return; + } + const parentEl = expNode._$parentEl!; + const flowIndex = getFlowIndexFromNodes(parentEl._$nodes, expNode); + const nextSibling = parentEl.childNodes[flowIndex]; + appendNodesWithSibling(newNodes, parentEl, nextSibling); + runDidMount(); + + expNode._$nodes = newNodes; +} + +function cache(expNode: ExpNode, deps: unknown[]) { + if (!deps || !deps.length) return false; + if (equal(deps, expNode.deps)) return true; + expNode.deps = deps; + return false; +} diff --git a/packages/inula-next/src/MutableNode/FlatNode.js b/packages/inula-next/src/MutableNode/FlatNode.js deleted file mode 100644 index 9f1519e8137f2d31c3b2b976cf1d4572aaf4cb1e..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/MutableNode/FlatNode.js +++ /dev/null @@ -1,33 +0,0 @@ -import { DLStore } from '../store'; -import { MutableNode } from './MutableNode'; - -export class FlatNode extends MutableNode { - willUnmountFuncs = []; - didUnmountFuncs = []; - - setUnmountFuncs() { - this.willUnmountFuncs = DLStore.global.WillUnmountStore.pop(); - this.didUnmountFuncs = DLStore.global.DidUnmountStore.pop(); - } - - runWillUnmount() { - for (let i = 0; i < this.willUnmountFuncs.length; i++) this.willUnmountFuncs[i](); - } - - runDidUnmount() { - for (let i = this.didUnmountFuncs.length - 1; i >= 0; i--) this.didUnmountFuncs[i](); - } - - removeNodes(nodes) { - this.runWillUnmount(); - super.removeNodes(nodes); - this.runDidUnmount(); - } - - geneNewNodesInEnv(newNodesFunc) { - this.initUnmountStore(); - const nodes = super.geneNewNodesInEnv(newNodesFunc); - this.setUnmountFuncs(); - return nodes; - } -} diff --git a/packages/inula-next/src/MutableNode/ForNode.js b/packages/inula-next/src/MutableNode/ForNode.js deleted file mode 100644 index b8f6e890b7f359365ab5bae62f0df5ffc8c10717..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/MutableNode/ForNode.js +++ /dev/null @@ -1,406 +0,0 @@ -import { DLNodeType } from '../DLNode'; -import { DLStore } from '../store'; -import { MutableNode } from './MutableNode'; - -export class ForNode extends MutableNode { - array; - nodeFunc; - depNum; - - nodesMap = new Map(); - updateArr = []; - - /** - * @brief Getter for nodes - */ - get _$nodes() { - const nodes = []; - for (let idx = 0; idx < this.array.length; idx++) { - nodes.push(...this.nodesMap.get(this.keys?.[idx] ?? idx)); - } - return nodes; - } - - /** - * @brief Constructor, For type - * @param array - * @param nodeFunc - * @param keys - */ - constructor(array, depNum, keys, nodeFunc) { - super(DLNodeType.For); - this.array = [...array]; - this.keys = keys; - this.depNum = depNum; - this.addNodeFunc(nodeFunc); - } - - /** - * @brief To be called immediately after the constructor - * @param nodeFunc - */ - addNodeFunc(nodeFunc) { - this.nodeFunc = nodeFunc; - this.array.forEach((item, idx) => { - this.initUnmountStore(); - const key = this.keys?.[idx] ?? idx; - const nodes = nodeFunc(item, idx, this.updateArr); - this.nodesMap.set(key, nodes); - this.setUnmountMap(key); - }); - // ---- For nested ForNode, the whole strategy is just like EnvStore - // we use array of function array to create "environment", popping and pushing - ForNode.addWillUnmount(this, this.runAllWillUnmount.bind(this)); - ForNode.addDidUnmount(this, this.runAllDidUnmount.bind(this)); - } - - /** - * @brief Update the view related to one item in the array - * @param nodes - * @param item - */ - updateItem(idx, array, changed) { - // ---- The update function of ForNode's childNodes is stored in the first child node - this.updateArr[idx]?.(changed ?? this.depNum, array[idx]); - } - - updateItems(changed) { - for (let idx = 0; idx < this.array.length; idx++) { - this.updateItem(idx, this.array, changed); - } - } - - /** - * @brief Non-array update function - * @param changed - */ - update(changed) { - // ---- e.g. this.depNum -> 1110 changed-> 1010 - // ~this.depNum & changed -> ~1110 & 1010 -> 0000 - // no update because depNum contains all the changed - // ---- e.g. this.depNum -> 1110 changed-> 1101 - // ~this.depNum & changed -> ~1110 & 1101 -> 0001 - // update because depNum doesn't contain all the changed - if (!(~this.depNum & changed)) return; - this.updateItems(changed); - } - - /** - * @brief Array-related update function - * @param newArray - * @param newKeys - */ - updateArray(newArray, newKeys) { - if (newKeys) { - this.updateWithKey(newArray, newKeys); - return; - } - this.updateWithOutKey(newArray); - } - - /** - * @brief Shortcut to generate new nodes with idx and key - */ - getNewNodes(idx, key, array, updateArr) { - this.initUnmountStore(); - const nodes = this.geneNewNodesInEnv(() => this.nodeFunc(array[idx], idx, updateArr ?? this.updateArr)); - this.setUnmountMap(key); - this.nodesMap.set(key, nodes); - return nodes; - } - - /** - * @brief Set the unmount map by getting the last unmount map from the global store - * @param key - */ - setUnmountMap(key) { - const willUnmountMap = DLStore.global.WillUnmountStore.pop(); - if (willUnmountMap && willUnmountMap.length > 0) { - if (!this.willUnmountMap) this.willUnmountMap = new Map(); - this.willUnmountMap.set(key, willUnmountMap); - } - const didUnmountMap = DLStore.global.DidUnmountStore.pop(); - if (didUnmountMap && didUnmountMap.length > 0) { - if (!this.didUnmountMap) this.didUnmountMap = new Map(); - this.didUnmountMap.set(key, didUnmountMap); - } - } - - /** - * @brief Run all the unmount functions and clear the unmount map - */ - runAllWillUnmount() { - if (!this.willUnmountMap || this.willUnmountMap.size === 0) return; - this.willUnmountMap.forEach(funcs => { - for (let i = 0; i < funcs.length; i++) funcs[i]?.(); - }); - this.willUnmountMap.clear(); - } - - /** - * @brief Run all the unmount functions and clear the unmount map - */ - runAllDidUnmount() { - if (!this.didUnmountMap || this.didUnmountMap.size === 0) return; - this.didUnmountMap.forEach(funcs => { - for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.(); - }); - this.didUnmountMap.clear(); - } - - /** - * @brief Run the unmount functions of the given key - * @param key - */ - runWillUnmount(key) { - if (!this.willUnmountMap || this.willUnmountMap.size === 0) return; - const funcs = this.willUnmountMap.get(key); - if (!funcs) return; - for (let i = 0; i < funcs.length; i++) funcs[i]?.(); - this.willUnmountMap.delete(key); - } - - /** - * @brief Run the unmount functions of the given key - */ - runDidUnmount(key) { - if (!this.didUnmountMap || this.didUnmountMap.size === 0) return; - const funcs = this.didUnmountMap.get(key); - if (!funcs) return; - for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.(); - this.didUnmountMap.delete(key); - } - - /** - * @brief Remove nodes from parentEl and run willUnmount and didUnmount - * @param nodes - * @param key - */ - removeNodes(nodes, key) { - this.runWillUnmount(key); - super.removeNodes(nodes); - this.runDidUnmount(key); - this.nodesMap.delete(key); - } - - /** - * @brief Update the nodes without keys - * @param newArray - */ - updateWithOutKey(newArray) { - const preLength = this.array.length; - const currLength = newArray.length; - - if (preLength === currLength) { - // ---- If the length is the same, we only need to update the nodes - for (let idx = 0; idx < this.array.length; idx++) { - this.updateItem(idx, newArray); - } - this.array = [...newArray]; - return; - } - const parentEl = this._$parentEl; - // ---- If the new array is longer, add new nodes directly - if (preLength < currLength) { - let flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this); - // ---- Calling parentEl.childNodes.length is time-consuming, - // so we use a length variable to store the length - const length = parentEl.childNodes.length; - for (let idx = 0; idx < currLength; idx++) { - if (idx < preLength) { - flowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(idx)); - this.updateItem(idx, newArray); - continue; - } - const newNodes = this.getNewNodes(idx, idx, newArray); - ForNode.appendNodesWithIndex(newNodes, parentEl, flowIndex, length); - } - ForNode.runDidMount(); - this.array = [...newArray]; - return; - } - - // ---- Update the nodes first - for (let idx = 0; idx < currLength; idx++) { - this.updateItem(idx, newArray); - } - // ---- If the new array is shorter, remove the extra nodes - for (let idx = currLength; idx < preLength; idx++) { - const nodes = this.nodesMap.get(idx); - this.removeNodes(nodes, idx); - } - this.updateArr.splice(currLength, preLength - currLength); - this.array = [...newArray]; - } - - /** - * @brief Update the nodes with keys - * @param newArray - * @param newKeys - */ - updateWithKey(newArray, newKeys) { - if (newKeys.length !== new Set(newKeys).size) { - throw new Error('Inula-Next: Duplicate keys in for loop are not allowed'); - } - const prevKeys = this.keys; - this.keys = newKeys; - - if (ForNode.arrayEqual(prevKeys, this.keys)) { - // ---- If the keys are the same, we only need to update the nodes - for (let idx = 0; idx < newArray.length; idx++) { - this.updateItem(idx, newArray); - } - this.array = [...newArray]; - return; - } - - const parentEl = this._$parentEl; - - // ---- No nodes after, delete all nodes - if (this.keys.length === 0) { - const parentNodes = parentEl._$nodes ?? []; - if (parentNodes.length === 1 && parentNodes[0] === this) { - // ---- ForNode is the only node in the parent node - // Frequently used in real life scenarios because we tend to always wrap for with a div element, - // so we optimize it here - this.runAllWillUnmount(); - parentEl.innerHTML = ''; - this.runAllDidUnmount(); - } else { - for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) { - const prevKey = prevKeys[prevIdx]; - this.removeNodes(this.nodesMap.get(prevKey), prevKey); - } - } - this.nodesMap.clear(); - this.updateArr = []; - this.array = []; - return; - } - - // ---- Record how many nodes are before this ForNode with the same parentNode - const flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this); - - // ---- No nodes before, append all nodes - if (prevKeys.length === 0) { - const nextSibling = parentEl.childNodes[flowIndex]; - for (let idx = 0; idx < this.keys.length; idx++) { - const newNodes = this.getNewNodes(idx, this.keys[idx], newArray); - ForNode.appendNodesWithSibling(newNodes, parentEl, nextSibling); - } - ForNode.runDidMount(); - this.array = [...newArray]; - return; - } - - const shuffleKeys = []; - const newUpdateArr = []; - - // ---- 1. Delete the nodes that are no longer in the array - for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) { - const prevKey = prevKeys[prevIdx]; - if (this.keys.includes(prevKey)) { - shuffleKeys.push(prevKey); - newUpdateArr.push(this.updateArr[prevIdx]); - continue; - } - this.removeNodes(this.nodesMap.get(prevKey), prevKey); - } - - // ---- 2. Add the nodes that are not in the array but in the new array - // ---- Calling parentEl.childNodes.length is time-consuming, - // so we use a length variable to store the length - let length = parentEl.childNodes.length; - let newFlowIndex = flowIndex; - for (let idx = 0; idx < this.keys.length; idx++) { - const key = this.keys[idx]; - const prevIdx = shuffleKeys.indexOf(key); - if (prevIdx !== -1) { - // ---- These nodes are already in the parentEl, - // and we need to keep track of their flowIndex - newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key)); - newUpdateArr[prevIdx]?.(this.depNum, newArray[idx]); - continue; - } - // ---- Insert updateArr first because in getNewNode the updateFunc will replace this null - newUpdateArr.splice(idx, 0, null); - const newNodes = this.getNewNodes(idx, key, newArray, newUpdateArr); - // ---- Add the new nodes - shuffleKeys.splice(idx, 0, key); - - const count = ForNode.appendNodesWithIndex(newNodes, parentEl, newFlowIndex, length); - newFlowIndex += count; - length += count; - } - ForNode.runDidMount(); - - // ---- After adding and deleting, the only thing left is to reorder the nodes, - // but if the keys are the same, we don't need to reorder - if (ForNode.arrayEqual(this.keys, shuffleKeys)) { - this.array = [...newArray]; - this.updateArr = newUpdateArr; - return; - } - - newFlowIndex = flowIndex; - const bufferNodes = new Map(); - // ---- 3. Replace the nodes in the same position using Fisher-Yates shuffle algorithm - for (let idx = 0; idx < this.keys.length; idx++) { - const key = this.keys[idx]; - const prevIdx = shuffleKeys.indexOf(key); - - const bufferedNode = bufferNodes.get(key); - if (bufferedNode) { - // ---- We need to add the flowIndex of the bufferedNode, - // because the bufferedNode is in the parentEl and the new position is ahead of the previous position - const bufferedFlowIndex = ForNode.getFlowIndexFromNodes(bufferedNode); - const lastEl = ForNode.toEls(bufferedNode).pop(); - const nextSibling = parentEl.childNodes[newFlowIndex + bufferedFlowIndex]; - if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) { - // ---- If the node is buffered, we need to add it to the parentEl - ForNode.insertNodesBefore(bufferedNode, parentEl, nextSibling); - } - // ---- So the added length is the length of the bufferedNode - newFlowIndex += bufferedFlowIndex; - delete bufferNodes[idx]; - } else if (prevIdx === idx) { - // ---- If the node is in the same position, we don't need to do anything - newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key)); - continue; - } else { - // ---- If the node is not in the same position, we need to buffer it - // We buffer the node of the previous position, and then replace it with the node of the current position - const prevKey = shuffleKeys[idx]; - bufferNodes.set(prevKey, this.nodesMap.get(prevKey)); - // ---- Length would never change, and the last will always be in the same position, - // so it'll always be insertBefore instead of appendChild - const childNodes = this.nodesMap.get(key); - const lastEl = ForNode.toEls(childNodes).pop(); - const nextSibling = parentEl.childNodes[newFlowIndex]; - if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) { - newFlowIndex += ForNode.insertNodesBefore(childNodes, parentEl, nextSibling); - } - } - // ---- Swap the keys - const tempKey = shuffleKeys[idx]; - shuffleKeys[idx] = shuffleKeys[prevIdx]; - shuffleKeys[prevIdx] = tempKey; - const tempUpdateFunc = newUpdateArr[idx]; - newUpdateArr[idx] = newUpdateArr[prevIdx]; - newUpdateArr[prevIdx] = tempUpdateFunc; - } - this.array = [...newArray]; - this.updateArr = newUpdateArr; - } - - /** - * @brief Compare two arrays - * @param arr1 - * @param arr2 - * @returns - */ - static arrayEqual(arr1, arr2) { - if (arr1.length !== arr2.length) return false; - return arr1.every((item, idx) => item === arr2[idx]); - } -} diff --git a/packages/inula-next/src/MutableNode/ForNode.ts b/packages/inula-next/src/MutableNode/ForNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..301173c590acd9e284feacf9ac743b8032f9edb5 --- /dev/null +++ b/packages/inula-next/src/MutableNode/ForNode.ts @@ -0,0 +1,373 @@ +import { InulaNodeType } from '@openinula/next-shared'; +import { + appendNodesWithIndex, + appendNodesWithSibling, + getFlowIndexFromNodes, + insertNodesBefore, + toEls, +} from '../InulaNode'; +import { addDidUnmount, addWillUnmount, endUnmountScope, runDidMount } from '../lifecycle'; +import { ForNode, InulaNode, VNode } from '../types'; +import { geneNewNodesInCtx, getSavedCtxNodes } from './mutableHandler'; +import { startUnmountScope } from '../lifecycle'; +import { removeNodes as removeMutableNodes } from './mutableHandler'; + +export function createForNode( + array: T[], + depNum: number, + keys: number[], + nodeFunc: (item: T, idx: number, updateArr: any[]) => VNode[] +) { + const forNode: ForNode = { + __type: InulaNodeType.For, + array: [...array], + depNum, + keys, + nodeFunc, + nodesMap: new Map(), + updateArr: [], + didUnmountFuncs: new Map(), + willUnmountFuncs: new Map(), + savedContextNodes: getSavedCtxNodes(), + get _$nodes() { + const nodes = []; + for (let idx = 0; idx < forNode.array.length; idx++) { + nodes.push(...forNode.nodesMap.get(forNode.keys?.[idx] ?? idx)!); + } + return nodes; + }, + }; + addNodeFunc(forNode, nodeFunc); + return forNode; +} + +/** + * @brief To be called immediately after the constructor + * @param forNode + * @param nodeFunc + */ +function addNodeFunc(forNode: ForNode, nodeFunc: (item: T, idx: number, updateArr: any[]) => VNode[]) { + forNode.array.forEach((item, idx) => { + startUnmountScope(); + const key = forNode.keys?.[idx] ?? idx; + const nodes = nodeFunc(item, idx, forNode.updateArr); + forNode.nodesMap.set(key, nodes); + setUnmountMap(forNode, key); + }); + + // ---- For nested ForNode, the whole strategy is just like EnvStore + // we use array of function array to create "environment", popping and pushing + addWillUnmount(forNode, () => runLifecycleMap(forNode.willUnmountFuncs)); + addDidUnmount(forNode, () => runLifecycleMap(forNode.didUnmountFuncs)); +} + +function runLifecycleMap(map: Map, key?: number) { + if (!map || map.size === 0) { + return; + } + if (typeof key === 'number') { + const funcs = map.get(key); + if (!funcs) return; + for (let i = 0; i < funcs.length; i++) funcs[i]?.(); + map.delete(key); + } else { + map.forEach(funcs => { + for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.(); + }); + map.clear(); + } +} + +/** + * @brief Set the unmount map by getting the last unmount map from the global store + * @param key + */ +function setUnmountMap(forNode: ForNode, key: number) { + const [willUnmountMap, didUnmountMap] = endUnmountScope(); + if (willUnmountMap && willUnmountMap.length > 0) { + if (!forNode.willUnmountFuncs) forNode.willUnmountFuncs = new Map(); + forNode.willUnmountFuncs.set(key, willUnmountMap); + } + if (didUnmountMap && didUnmountMap.length > 0) { + if (!forNode.didUnmountFuncs) forNode.didUnmountFuncs = new Map(); + forNode.didUnmountFuncs.set(key, didUnmountMap); + } +} + +/** + * @brief Non-array update function, invoke children's update function + * @param changed + */ +export function updateForChildren(forNode: ForNode, changed: number) { + // ---- e.g. this.depNum -> 1110 changed-> 1010 + // ~this.depNum & changed -> ~1110 & 1010 -> 0000 + // no update because depNum contains all the changed + // ---- e.g. this.depNum -> 1110 changed-> 1101 + // ~this.depNum & changed -> ~1110 & 1101 -> 000f1 + // update because depNum doesn't contain all the changed + if (!(~forNode.depNum & changed)) return; + for (let idx = 0; idx < forNode.array.length; idx++) { + updateItem(forNode, idx, forNode.array, changed); + } +} + +/** + * @brief Update the view related to one item in the array + * @param forNode - The ForNode + * @param idx - The index of the item in the array + * @param array - The array of items + * @param changed - The changed bit + */ +function updateItem(forNode: ForNode, idx: number, array: T[], changed?: number) { + // ---- The update function of ForNode's childNodes is stored in the first child node + forNode.updateArr[idx]?.(changed ?? forNode.depNum, array[idx]); +} + +/** + * @brief Array-related update function + */ +export function updateForNode(forNode: ForNode, newArray: T[], newKeys: number[]) { + if (newKeys) { + updateWithKey(forNode, newArray, newKeys); + return; + } + updateWithOutKey(forNode, newArray); +} + +/** + * @brief Shortcut to generate new nodes with idx and key + */ +function getNewNodes(forNode: ForNode, idx: number, key: number, array: T[], updateArr?: any[]) { + startUnmountScope(); + const nodes = geneNewNodesInCtx(forNode, () => forNode.nodeFunc(array[idx], idx, updateArr ?? forNode.updateArr)); + setUnmountMap(forNode, key); + forNode.nodesMap.set(key, nodes); + return nodes; +} + +/** + * @brief Remove nodes from parentEl and run willUnmount and didUnmount + * @param nodes + * @param key + */ +function removeNodes(forNode: ForNode, nodes: InulaNode[], key: number) { + runLifecycleMap(forNode.willUnmountFuncs, key); + removeMutableNodes(forNode, nodes); + runLifecycleMap(forNode.didUnmountFuncs, key); + forNode.nodesMap.delete(key); +} + +/** + * @brief Update the nodes without keys + * @param newArray + */ +function updateWithOutKey(forNode: ForNode, newArray: T[]) { + const preLength = forNode.array.length; + const currLength = newArray.length; + + if (preLength === currLength) { + // ---- If the length is the same, we only need to update the nodes + for (let idx = 0; idx < forNode.array.length; idx++) { + updateItem(forNode, idx, newArray); + } + forNode.array = [...newArray]; + return; + } + const parentEl = forNode._$parentEl!; + // ---- If the new array is longer, add new nodes directly + if (preLength < currLength) { + let flowIndex = getFlowIndexFromNodes(parentEl._$nodes, forNode); + // ---- Calling parentEl.childNodes.length is time-consuming, + // so we use a length variable to store the length + const length = parentEl.childNodes.length; + for (let idx = 0; idx < currLength; idx++) { + if (idx < preLength) { + flowIndex += getFlowIndexFromNodes(forNode.nodesMap.get(idx)!); + updateItem(forNode, idx, newArray); + continue; + } + const newNodes = getNewNodes(forNode, idx, idx, newArray); + appendNodesWithIndex(newNodes, parentEl, flowIndex, length); + } + runDidMount(); + forNode.array = [...newArray]; + return; + } + + // ---- Update the nodes first + for (let idx = 0; idx < currLength; idx++) { + updateItem(forNode, idx, newArray); + } + // ---- If the new array is shorter, remove the extra nodes + for (let idx = currLength; idx < preLength; idx++) { + const nodes = forNode.nodesMap.get(idx); + removeNodes(forNode, nodes!, idx); + } + forNode.updateArr.splice(currLength, preLength - currLength); + forNode.array = [...newArray]; +} + +function arrayEqual(arr1: T[], arr2: T[]) { + if (arr1.length !== arr2.length) return false; + return arr1.every((item, idx) => item === arr2[idx]); +} + +/** + * @brief Update the nodes with keys + * @param newArray + * @param newKeys + */ +function updateWithKey(forNode: ForNode, newArray: T[], newKeys: number[]) { + if (newKeys.length !== new Set(newKeys).size) { + throw new Error('Inula-Next: Duplicate keys in for loop are not allowed'); + } + const prevKeys = forNode.keys; + forNode.keys = newKeys; + + if (arrayEqual(prevKeys, newKeys)) { + // ---- If the keys are the same, we only need to update the nodes + for (let idx = 0; idx < newArray.length; idx++) { + updateItem(forNode, idx, newArray); + } + forNode.array = [...newArray]; + return; + } + + const parentEl = forNode._$parentEl!; + + // ---- No nodes after, delete all nodes + if (newKeys.length === 0) { + const parentNodes = parentEl._$nodes ?? []; + if (parentNodes.length === 1 && parentNodes[0] === forNode) { + // ---- ForNode is the only node in the parent node + // Frequently used in real life scenarios because we tend to always wrap for with a div element, + // so we optimize it here + runLifecycleMap(forNode.willUnmountFuncs); + parentEl.innerHTML = ''; + runLifecycleMap(forNode.didUnmountFuncs); + } else { + for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) { + const prevKey = prevKeys[prevIdx]; + removeNodes(forNode, forNode.nodesMap.get(prevKey)!, prevKey); + } + } + forNode.nodesMap.clear(); + forNode.updateArr = []; + forNode.array = []; + return; + } + + // ---- Record how many nodes are before this ForNode with the same parentNode + const flowIndex = getFlowIndexFromNodes(parentEl._$nodes, forNode); + + // ---- No nodes before, append all nodes + if (prevKeys.length === 0) { + const nextSibling = parentEl.childNodes[flowIndex]; + for (let idx = 0; idx < newKeys.length; idx++) { + const newNodes = getNewNodes(forNode, idx, newKeys[idx], newArray); + appendNodesWithSibling(newNodes, parentEl, nextSibling); + } + runDidMount(); + forNode.array = [...newArray]; + return; + } + + const shuffleKeys: number[] = []; + const newUpdateArr: any[] = []; + + // ---- 1. Delete the nodes that are no longer in the array + for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) { + const prevKey = prevKeys[prevIdx]; + if (forNode.keys.includes(prevKey)) { + shuffleKeys.push(prevKey); + newUpdateArr.push(forNode.updateArr[prevIdx]); + continue; + } + removeNodes(forNode, forNode.nodesMap.get(prevKey)!, prevKey); + } + + // ---- 2. Add the nodes that are not in the array but in the new array + // ---- Calling parentEl.childNodes.length is time-consuming, + // so we use a length variable to store the length + let length = parentEl.childNodes.length; + let newFlowIndex = flowIndex; + for (let idx = 0; idx < forNode.keys.length; idx++) { + const key = forNode.keys[idx]; + const prevIdx = shuffleKeys.indexOf(key); + if (prevIdx !== -1) { + // ---- These nodes are already in the parentEl, + // and we need to keep track of their flowIndex + newFlowIndex += getFlowIndexFromNodes(forNode.nodesMap.get(key)!); + newUpdateArr[prevIdx]?.(forNode.depNum, newArray[idx]); + continue; + } + // ---- Insert updateArr first because in getNewNode the updateFunc will replace this null + newUpdateArr.splice(idx, 0, null); + const newNodes = getNewNodes(forNode, idx, key, newArray); + // ---- Add the new nodes + shuffleKeys.splice(idx, 0, key); + + const count = appendNodesWithIndex(newNodes, parentEl, newFlowIndex, length); + newFlowIndex += count; + length += count; + } + runDidMount(); + + // ---- After adding and deleting, the only thing left is to reorder the nodes, + // but if the keys are the same, we don't need to reorder + if (arrayEqual(forNode.keys, shuffleKeys)) { + forNode.array = [...newArray]; + forNode.updateArr = newUpdateArr; + return; + } + + newFlowIndex = flowIndex; + const bufferNodes = new Map(); + // ---- 3. Replace the nodes in the same position using Fisher-Yates shuffle algorithm + for (let idx = 0; idx < forNode.keys.length; idx++) { + const key = forNode.keys[idx]; + const prevIdx = shuffleKeys.indexOf(key); + + const bufferedNode = bufferNodes.get(key); + if (bufferedNode) { + // ---- We need to add the flowIndex of the bufferedNode, + // because the bufferedNode is in the parentEl and the new position is ahead of the previous position + const bufferedFlowIndex = getFlowIndexFromNodes(bufferedNode); + const lastEl = toEls(bufferedNode).pop()!; + const nextSibling = parentEl.childNodes[newFlowIndex + bufferedFlowIndex]; + if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) { + // ---- If the node is buffered, we need to add it to the parentEl + insertNodesBefore(bufferedNode, parentEl, nextSibling); + } + // ---- So the added length is the length of the bufferedNode + newFlowIndex += bufferedFlowIndex; + // TODO: ?? delete bufferNodes[idx]; + } else if (prevIdx === idx) { + // ---- If the node is in the same position, we don't need to do anything + newFlowIndex += getFlowIndexFromNodes(forNode.nodesMap.get(key)!); + continue; + } else { + // ---- If the node is not in the same position, we need to buffer it + // We buffer the node of the previous position, and then replace it with the node of the current position + const prevKey = shuffleKeys[idx]; + bufferNodes.set(prevKey, forNode.nodesMap.get(prevKey)!); + // ---- Length would never change, and the last will always be in the same position, + // so it'll always be insertBefore instead of appendChild + const childNodes = forNode.nodesMap.get(key)!; + const lastEl = toEls(childNodes).pop()!; + const nextSibling = parentEl.childNodes[newFlowIndex]; + if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) { + newFlowIndex += insertNodesBefore(childNodes, parentEl, nextSibling); + } + } + // ---- Swap the keys + const tempKey = shuffleKeys[idx]; + shuffleKeys[idx] = shuffleKeys[prevIdx]; + shuffleKeys[prevIdx] = tempKey; + const tempUpdateFunc = newUpdateArr[idx]; + newUpdateArr[idx] = newUpdateArr[prevIdx]; + newUpdateArr[prevIdx] = tempUpdateFunc; + } + forNode.array = [...newArray]; + forNode.updateArr = newUpdateArr; +} diff --git a/packages/inula-next/src/MutableNode/MutableNode.js b/packages/inula-next/src/MutableNode/MutableNode.js deleted file mode 100644 index 04521c97d76ab81279ba08d26e1677d73035b56a..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/MutableNode/MutableNode.js +++ /dev/null @@ -1,73 +0,0 @@ -import { DLNode } from '../DLNode'; -import { DLStore } from '../store'; -import { replaceEnvNodes } from '../ContextProvider.js'; - -export class MutableNode extends DLNode { - /** - * @brief Mutable node is a node that this._$nodes can be changed, things need to pay attention: - * 1. The environment of the new nodes should be the same as the old nodes - * 2. The new nodes should be added to the parentEl - * 3. The old nodes should be removed from the parentEl - * @param type - */ - constructor(type) { - super(type); - // ---- Save the current environment nodes, must be a new reference - const envNodeMap = DLStore.global.envNodeMap; - if (envNodeMap) { - this.savedEnvNodes = new Map([...envNodeMap]); - } - } - - /** - * @brief Initialize the new nodes, add parentEl to all nodes - * @param nodes - */ - initNewNodes(nodes) { - // ---- Add parentEl to all nodes - DLNode.addParentEl(nodes, this._$parentEl); - } - - /** - * @brief Generate new nodes in the saved environment - * @param newNodesFunc - * @returns - */ - geneNewNodesInEnv(newNodesFunc) { - if (!this.savedEnvNodes) { - // ---- No saved environment, just generate new nodes - const newNodes = newNodesFunc(); - // ---- Only for IfNode's same condition return - // ---- Initialize the new nodes - this.initNewNodes(newNodes); - return newNodes; - } - // ---- Save the current environment nodes - const currentEnvNodes = DLStore.global.envNodeMap; - // ---- Replace the saved environment nodes - replaceEnvNodes(this.savedEnvNodes); - const newNodes = newNodesFunc(); - // ---- Retrieve the current environment nodes - replaceEnvNodes(currentEnvNodes); - // ---- Only for IfNode's same condition return - // ---- Initialize the new nodes - this.initNewNodes(newNodes); - return newNodes; - } - - initUnmountStore() { - DLStore.global.WillUnmountStore.push([]); - DLStore.global.DidUnmountStore.push([]); - } - - /** - * @brief Remove nodes from parentEl and run willUnmount and didUnmount - * @param nodes - * @param removeEl Only remove outermost element - */ - removeNodes(nodes) { - DLNode.loopShallowEls(nodes, node => { - this._$parentEl.removeChild(node); - }); - } -} diff --git a/packages/inula-next/src/MutableNode/TryNode.js b/packages/inula-next/src/MutableNode/TryNode.js deleted file mode 100644 index 956d31b063905dac5c9f2ebc95e9dbfcfcf11925..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/MutableNode/TryNode.js +++ /dev/null @@ -1,45 +0,0 @@ -import { DLNodeType } from '../DLNode'; -import { FlatNode } from './FlatNode'; -import { ContextProvider } from '../ContextProvider.js'; - -export class TryNode extends FlatNode { - constructor(tryFunc, catchFunc) { - super(DLNodeType.Try); - this.tryFunc = tryFunc; - const catchable = this.getCatchable(catchFunc); - this.envNode = new ContextProvider({ _$catchable: catchable }); - const nodes = tryFunc(this.setUpdateFunc.bind(this), catchable) ?? []; - this.envNode.initNodes(nodes); - this._$nodes = nodes; - } - - update(changed) { - this.updateFunc?.(changed); - } - - setUpdateFunc(updateFunc) { - this.updateFunc = updateFunc; - } - - getCatchable(catchFunc) { - return callback => - (...args) => { - try { - return callback(...args); - } catch (e) { - // ---- Run it in next tick to make sure when error occurs before - // didMount, this._$parentEl is not null - Promise.resolve().then(() => { - const nodes = this.geneNewNodesInEnv(() => catchFunc(this.setUpdateFunc.bind(this), e)); - this._$nodes && this.removeNodes(this._$nodes); - const parentEl = this._$parentEl; - const flowIndex = FlatNode.getFlowIndexFromNodes(parentEl._$nodes, this); - const nextSibling = parentEl.childNodes[flowIndex]; - FlatNode.appendNodesWithSibling(nodes, parentEl, nextSibling); - FlatNode.runDidMount(); - this._$nodes = nodes; - }); - } - }; - } -} diff --git a/packages/inula-next/src/MutableNode/mutableHandler.ts b/packages/inula-next/src/MutableNode/mutableHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..b2e00b65cfe04985d4f16c0250a48ff114523b3b --- /dev/null +++ b/packages/inula-next/src/MutableNode/mutableHandler.ts @@ -0,0 +1,81 @@ +import { MutableNode, VNode, ScopedLifecycle, InulaNode, ContextNode } from '../types'; +import { endUnmountScope, startUnmountScope } from '../lifecycle'; +import { getContextNodeMap, replaceContext } from '../ContextNode'; +import { addParentEl, loopShallowEls } from '../InulaNode'; + +export function setUnmountFuncs(node: MutableNode) { + // pop will not be undefined,cause we push empty array when create node + const [willUnmountFuncs, didUnmountFuncs] = endUnmountScope(); + node.willUnmountFuncs = willUnmountFuncs!; + node.didUnmountFuncs = didUnmountFuncs!; +} + +export function runLifeCycle(fn: ScopedLifecycle) { + for (let i = 0; i < fn.length; i++) { + fn[i](); + } +} + +export function removeNodesWithUnmount(node: MutableNode, children: InulaNode[]) { + runLifeCycle(node.willUnmountFuncs); + removeNodes(node, children); + runLifeCycle(node.didUnmountFuncs); +} + +export function geneNewNodesInEnvWithUnmount(node: MutableNode, newNodesFunc: () => VNode[]) { + startUnmountScope(); + const nodes = geneNewNodesInCtx(node, newNodesFunc); + setUnmountFuncs(node); + return nodes; +} +export function getSavedCtxNodes(): Map> | null { + const contextNodeMap = getContextNodeMap(); + if (contextNodeMap) { + return new Map([...contextNodeMap]); + } + return null; +} +/** + * @brief Initialize the new nodes, add parentEl to all nodes + */ +function initNewNodes(node: VNode, children: Array) { + // ---- Add parentEl to all children + addParentEl(children, node._$parentEl!); +} +/** + * @brief Generate new nodes in the saved context + * @param node + * @param newNodesFunc + * @returns + */ + +export function geneNewNodesInCtx(node: MutableNode, newNodesFunc: () => Array) { + if (!node.savedContextNodes) { + // ---- No saved context, just generate new nodes + const newNodes = newNodesFunc(); + // ---- Only for IfNode's same condition return + // ---- Initialize the new nodes + initNewNodes(node, newNodes); + return newNodes; + } + // ---- Save the current context nodes + const currentContextNodes = getContextNodeMap()!; + // ---- Replace the saved context nodes + replaceContext(node.savedContextNodes); + const newNodes = newNodesFunc(); + // ---- Retrieve the current context nodes + replaceContext(currentContextNodes); + // ---- Only for IfNode's same condition return + // ---- Initialize the new nodes + initNewNodes(node, newNodes); + return newNodes; +} + +/** + * @brief Remove nodes from parentEl and run willUnmount and didUnmount + */ +export function removeNodes(node: VNode, children: InulaNode[]) { + loopShallowEls(children, dom => { + node._$parentEl!.removeChild(dom); + }); +} diff --git a/packages/inula-next/src/PropView.js b/packages/inula-next/src/PropView.js deleted file mode 100644 index 2cb1701f7bdfe6e5d805a73a0b1af9f6772f073c..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/PropView.js +++ /dev/null @@ -1,48 +0,0 @@ -import { DLNode } from './DLNode'; -import { insertNode } from './HTMLNode'; -export class PropView { - propViewFunc; - dlUpdateFunc = new Set(); - - /** - * @brief PropView constructor, accept a function that returns a list of DLNode - * @param propViewFunc - A function that when called, collects and returns an array of DLNode instances - */ - constructor(propViewFunc) { - this.propViewFunc = propViewFunc; - } - - /** - * @brief Build the prop view by calling the propViewFunc and add every single instance of the returned DLNode to dlUpdateNodes - * @returns An array of DLNode instances returned by propViewFunc - */ - build() { - let update; - const addUpdate = updateFunc => { - update = updateFunc; - this.dlUpdateFunc.add(updateFunc); - }; - const newNodes = this.propViewFunc(addUpdate); - if (newNodes.length === 0) return []; - if (update) { - // Remove the updateNode from dlUpdateNodes when it unmounts - DLNode.addWillUnmount(newNodes[0], this.dlUpdateFunc.delete.bind(this.dlUpdateFunc, update)); - } - - return newNodes; - } - - /** - * @brief Update every node in dlUpdateNodes - * @param changed - A parameter indicating what changed to trigger the update - */ - update(...args) { - this.dlUpdateFunc.forEach(update => { - update(...args); - }); - } -} - -export function insertChildren(el, propView) { - insertNode(el, { _$nodes: propView.build(), _$dlNodeType: 7 }, 0); -} diff --git a/packages/inula-next/src/TextNode.js b/packages/inula-next/src/TextNode.js deleted file mode 100644 index 5a55bee930b6f5888a22c31cabc71b25a9543df2..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/TextNode.js +++ /dev/null @@ -1,24 +0,0 @@ -import { DLStore, cached } from './store'; - -/** - * @brief Shorten document.createTextNode - * @param value - * @returns Text - */ -export function createTextNode(value, deps) { - const node = DLStore.document.createTextNode(value); - node.$$deps = deps; - return node; -} - -/** - * @brief Update text node and check if the value is changed - * @param node - * @param value - */ -export function updateText(node, valueFunc, deps) { - if (cached(deps, node.$$deps)) return; - const value = valueFunc(); - node.textContent = value; - node.$$deps = deps; -} diff --git a/packages/inula-next/src/equal.ts b/packages/inula-next/src/equal.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5a27fe180a1c243653530e90643ad18a73eb7ea --- /dev/null +++ b/packages/inula-next/src/equal.ts @@ -0,0 +1,10 @@ +/** + * @brief Shallowly compare the deps with the previous deps + * @param deps + * @param prevDeps + * @returns + */ +export function equal(deps: any[], prevDeps: any[]) { + if (!prevDeps || deps.length !== prevDeps.length) return false; + return deps.every((dep, i) => !(dep instanceof Object) && prevDeps[i] === dep); +} diff --git a/packages/inula-next/src/index.d.ts b/packages/inula-next/src/index.d.ts deleted file mode 100644 index a3d3b4a0d65741720a4fdad4521e99177c44c4d4..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './types/index'; diff --git a/packages/inula-next/src/index.js b/packages/inula-next/src/index.js deleted file mode 100644 index 6f77ecc6f1be4bf873e0dcaa30b5109b066bf02c..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/index.js +++ /dev/null @@ -1,179 +0,0 @@ -import { DLNode } from './DLNode'; -import { insertNode } from './HTMLNode'; -import { cached, DLStore } from './store'; -import { CompNode } from './CompNode.js'; -import { HookNode } from './HookNode.js'; - -export * from './HTMLNode'; -export * from './CompNode'; -export * from './ContextProvider.js'; -export * from './TextNode'; -export * from './PropView'; -export * from './MutableNode/ForNode'; -export * from './MutableNode/ExpNode'; -export * from './MutableNode/CondNode'; -export * from './MutableNode/TryNode'; - -export { setGlobal, setDocument } from './store'; - -function initStore() { - // Declare a global variable to store willUnmount functions - DLStore.global.WillUnmountStore = []; - // Declare a global variable to store didUnmount functions - DLStore.global.DidUnmountStore = []; -} - -/** - * @brief Render the DL class to the element - * @param {typeof import('./CompNode').CompNode} compFn - * @param {HTMLElement | string} idOrEl - */ -export function render(compFn, idOrEl) { - let el = idOrEl; - if (typeof idOrEl === 'string') { - const elFound = DLStore.document.getElementById(idOrEl); - if (elFound) el = elFound; - else { - throw new Error(`Inula-Next: Element with id ${idOrEl} not found`); - } - } - initStore(); - el.innerHTML = ''; - const dlNode = Comp(compFn); - insertNode(el, dlNode, 0); - DLNode.runDidMount(); -} - -export function untrack(callback) { - return callback(); -} - -export const required = null; - -export function use() { - console.error( - 'Inula-Next: use() is not supported be called directly. You can only assign `use(model)` to a Inula-Next class property. Any other expressions are not allowed.' - ); -} - -let currentComp = null; - -export function inMount() { - return !!currentComp; -} - -/** - * @typedef compUpdator - * @property {(bit: number) => void} updateState - * @property {(propName: string, newValue: any) => void} updateProp - * @property {() => ([HTMLElement[], (bit: number) => HTMLElement[]])} getUpdateViews - * @property {(newValue: any, bit: number) => {} updateDerived - */ -export function Comp(compFn, props = {}) { - if (props['*spread*']) { - props = { ...props, ...props['*spread*'] }; - } - return mountNode(() => new CompNode(), compFn, props); -} - -function mountNode(ctor, compFn, props) { - const compNode = ctor(); - let prevNode = currentComp; - try { - currentComp = compNode; - compFn(props); - } catch (err) { - throw err; - } finally { - currentComp = prevNode; - } - return compNode; -} - -/** - * @brief Create a component - * @param {compUpdator} compUpdater - * @return {*} - */ -export function createComponent(compUpdater) { - if (!currentComp) { - throw new Error('Should not call createComponent outside the component function'); - } - currentComp.setUpdateFunc(compUpdater); - return currentComp; -} - -export function notCached(node, cacheSymbol, cacheValues) { - if (!cacheValues || !cacheValues.length) return false; - if (!node.$nonkeyedCache) { - node.$nonkeyedCache = {}; - } - if (!cached(cacheValues, node.$nonkeyedCache[cacheSymbol])) { - return true; - } - node.$nonkeyedCache[cacheSymbol] = cacheValues; - return false; -} - -export function didMount() { - throw new Error('lifecycle should be compiled, check the babel plugin'); -} - -export function willUnmount() { - throw new Error('lifecycle should be compiled, check the babel plugin'); -} - -export function didUnMount() { - throw new Error('lifecycle should be compiled, check the babel plugin'); -} - -export function createContext(defaultVal) { - return { - id: Symbol('inula-ctx'), - value: defaultVal, - }; -} - -export function useContext(ctx, key) { - const envNodeMap = DLStore.global.envNodeMap; - if (envNodeMap) { - const envNode = envNodeMap.get(ctx.id); - if (envNode) { - envNode.addNode(currentComp); - } - } - - if (key) { - return ctx.value[key]; - } - - return ctx.value; -} - -/** - * - * @param {(props: Record) => any} hookFn - * @param {any[]}params - * @param {number}bitMap - */ -export function useHook(hookFn, params, bitMap) { - if (currentComp) { - const props = params.reduce((obj, val, idx) => ({ ...obj, [`p${idx}`]: val }), {}); - // if there is the currentComp means we are mounting the component tree - return mountNode(() => new HookNode(currentComp, bitMap), hookFn, props); - } -} - -export function createHook(compUpdater) { - if (!currentComp) { - throw new Error('Should not call createComponent outside the component function'); - } - currentComp.setUpdateFunc(compUpdater); - return currentComp; -} - -export function runOnce(fn) { - if (currentComp) { - fn(); - } -} diff --git a/packages/inula-next/src/index.ts b/packages/inula-next/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..87ba18872a44e23c8524bba1891b69ee2462e2d7 --- /dev/null +++ b/packages/inula-next/src/index.ts @@ -0,0 +1,199 @@ +import { runDidMount } from './lifecycle'; +import { insertNode } from './renderer/dom'; +import { equal } from './equal'; +import { constructComp, createCompNode, updateCompNode } from './CompNode'; +import { constructHook, createHookNode } from './HookNode'; +import { CompNode, VNode, InulaHTMLNode, HookNode, ChildrenNode } from './types'; +import { createContextNode, updateContextNode } from './ContextNode'; +import { InulaNodeType } from '@openinula/next-shared'; +import { createChildrenNode, updateChildrenNode } from './ChildrenNode'; +import { createForNode, updateForChildren, updateForNode } from './MutableNode/ForNode'; +import { createExpNode, updateExpNode } from './MutableNode/ExpNode'; +import { createCondNode, updateCondChildren, updateCondNode } from './MutableNode/CondNode'; + +export * from './renderer/dom'; +export * from './CompNode'; +export * from './ContextNode'; +export * from './MutableNode/ForNode'; +export * from './MutableNode/ExpNode'; +export * from './MutableNode/CondNode'; + +export type FunctionComponent = (props: Record) => CompNode | HookNode; + +export function render(compFn: FunctionComponent, container: HTMLElement): void { + if (container == null) { + throw new Error('Render target is empty. Please provide a valid DOM element.'); + } + container.innerHTML = ''; + const node = Comp(compFn); + insertNode(container as InulaHTMLNode, node, 0); + runDidMount(); +} + +// export function unmount(container: InulaHTMLNode): void { +// const node = container.firstChild; +// if (node) { +// removeNode(node); +// } +// } + +export function untrack(callback: () => V): V { + return callback(); +} + +export let currentComp: CompNode | HookNode | null = null; + +export function inMount(): boolean { + return !!currentComp; +} + +interface CompUpdater { + updateState: (bit: number) => void; + updateProp: (propName: string, newValue: unknown) => void; + getUpdateViews: () => [HTMLElement[], (bit: number) => HTMLElement[]]; + updateDerived: (newValue: unknown, bit: number) => void; +} + +export function Comp(compFn: FunctionComponent, props: Record = {}): CompNode { + return mountNode(() => createCompNode(), compFn, props); +} + +function mountNode( + ctor: () => T, + compFn: FunctionComponent, + props: Record +): T { + const compNode = ctor(); + const prevNode = currentComp; + try { + currentComp = compNode; + compFn(props); + // eslint-disable-next-line no-useless-catch + } catch (err) { + throw err; + } finally { + currentComp = prevNode; + } + return compNode; +} + +export function createComponent(compUpdater: CompUpdater): CompNode { + if (!currentComp || currentComp.__type !== InulaNodeType.Comp) { + throw new Error('Should not call createComponent outside the component function'); + } + constructComp(currentComp, compUpdater); + return currentComp; +} + +export function notCached(node: VNode, cacheId: string, cacheValues: unknown[]): boolean { + if (!cacheValues || !cacheValues.length) return false; + if (!node.$nonkeyedCache) { + node.$nonkeyedCache = {}; + } + if (!equal(cacheValues, node.$nonkeyedCache[cacheId])) { + node.$nonkeyedCache[cacheId] = cacheValues; + return true; + } + return false; +} + +export function didMount(fn: () => void) { + throw new Error('lifecycle should be compiled, check the babel plugin'); +} + +export function willUnmount(fn: () => void) { + throw new Error('lifecycle should be compiled, check the babel plugin'); +} + +export function didUnMount(fn: () => void) { + throw new Error('lifecycle should be compiled, check the babel plugin'); +} + +export function useHook(hookFn: (props: Record) => HookNode, params: unknown[], bitMap: number) { + if (currentComp) { + const props = params.reduce>((obj, val, idx) => ({ ...obj, [`p${idx}`]: val }), {}); + // if there is the currentComp means we are mounting the component tree + return mountNode(() => createHookNode(currentComp!, bitMap), hookFn, props); + } + throw new Error('useHook must be called within a component'); +} + +export function createHook(compUpdater: CompUpdater): HookNode { + if (!currentComp || currentComp.__type !== InulaNodeType.Hook) { + throw new Error('Should not call createComponent outside the component function'); + } + constructHook(currentComp, compUpdater); + return currentComp; +} + +export function runOnce(fn: () => void): void { + if (currentComp) { + fn(); + } +} + +export function createNode(type: InulaNodeType, ...args: unknown[]): VNode | ChildrenNode { + switch (type) { + case InulaNodeType.Context: + return createContextNode(...(args as Parameters)); + case InulaNodeType.Children: + return createChildrenNode(...(args as Parameters)); + case InulaNodeType.Comp: + return createCompNode(...(args as Parameters)); + case InulaNodeType.Hook: + return createHookNode(...(args as Parameters)); + case InulaNodeType.For: + return createForNode(...(args as Parameters)); + case InulaNodeType.Cond: + return createCondNode(...(args as Parameters)); + case InulaNodeType.Exp: + return createExpNode(...(args as Parameters)); + default: + throw new Error(`Unsupported node type: ${type}`); + } +} + +export function updateNode(...args: unknown[]) { + const node = args[0] as VNode; + switch (node.__type) { + case InulaNodeType.Context: + updateContextNode(...(args as Parameters)); + break; + case InulaNodeType.Children: + updateChildrenNode(...(args as Parameters)); + break; + case InulaNodeType.For: + updateForNode(...(args as Parameters)); + break; + case InulaNodeType.Cond: + updateCondNode(...(args as Parameters)); + break; + case InulaNodeType.Exp: + updateExpNode(...(args as Parameters)); + break; + case InulaNodeType.Comp: + case InulaNodeType.Hook: + updateCompNode(...(args as Parameters)); + break; + default: + throw new Error(`Unsupported node type: ${node.__type}`); + } +} + +export function updateChildren(...args: unknown[]) { + const node = args[0] as VNode; + switch (node.__type) { + case InulaNodeType.For: + updateForChildren(...(args as Parameters)); + break; + case InulaNodeType.Cond: + updateCondChildren(...(args as Parameters)); + break; + default: + throw new Error(`Unsupported node type: ${node.__type}`); + } +} + +export { initContextChildren, createContext, useContext } from './ContextNode'; +export { initCompNode } from './CompNode'; +export { emitUpdate } from './HookNode'; diff --git a/packages/inula-next/src/lifecycle.ts b/packages/inula-next/src/lifecycle.ts new file mode 100644 index 0000000000000000000000000000000000000000..5be0ef89e220fdaa61fc37af7ab2065a3b0c3938 --- /dev/null +++ b/packages/inula-next/src/lifecycle.ts @@ -0,0 +1,41 @@ +import { VNode, ScopedLifecycle } from './types'; + +let DidMountStore: ScopedLifecycle; +const WillUnmountStore: ScopedLifecycle[] = []; +const DidUnmountStore: ScopedLifecycle[] = []; + +export const addWillUnmount = (node: VNode, func: (node: VNode) => void): void => { + const willUnmountStore = WillUnmountStore; + const currentStore = willUnmountStore[willUnmountStore.length - 1]; + if (!currentStore) return; + currentStore.push(() => func(node)); +}; + +export const addDidUnmount = (node: VNode, func: (node: VNode) => void): void => { + const didUnmountStore = DidUnmountStore; + const currentStore = didUnmountStore[didUnmountStore.length - 1]; + if (!currentStore) return; + currentStore.push(() => func(node)); +}; +export const addDidMount = (node: VNode, func: (node: VNode) => void): void => { + if (!DidMountStore) DidMountStore = []; + DidMountStore.push(() => func(node)); +}; + +export const runDidMount = (): void => { + const didMountStore = DidMountStore; + if (!didMountStore || didMountStore.length === 0) return; + for (let i = didMountStore.length - 1; i >= 0; i--) { + didMountStore[i](); + } + DidMountStore = []; +}; + +export function startUnmountScope() { + WillUnmountStore.push([]); + DidUnmountStore.push([]); +} + +export function endUnmountScope() { + return [WillUnmountStore.pop(), DidUnmountStore.pop()]; +} diff --git a/packages/inula-next/src/HTMLNode.js b/packages/inula-next/src/renderer/dom.ts similarity index 52% rename from packages/inula-next/src/HTMLNode.js rename to packages/inula-next/src/renderer/dom.ts index 70f999e6cb2f1e41e80f5779436a0d266233381a..39803d6fbefdcc5167401c38d2fe96fe1cff4a41 100644 --- a/packages/inula-next/src/HTMLNode.js +++ b/packages/inula-next/src/renderer/dom.ts @@ -1,212 +1,222 @@ -import { DLNode } from './DLNode'; -import { DLStore, cached } from './store'; - -function cache(el, key, deps) { - if (deps.length === 0) return false; - const cacheKey = `$${key}`; - if (cached(deps, el[cacheKey])) return true; - el[cacheKey] = deps; - return false; -} - -const isCustomProperty = name => name.startsWith('--'); - -/** - * TODO: share logic with legacy inula - * @brief Plainly set style - * @param el - * @param newStyle - */ -export function setStyle(el, newStyle) { - const style = el.style; - const prevStyle = el._prevStyle || {}; - - // Remove styles that are no longer present - for (const key in prevStyle) { - // eslint-disable-next-line no-prototype-builtins - if (prevStyle.hasOwnProperty(key) && (newStyle == null || !newStyle.hasOwnProperty(key))) { - if (isCustomProperty(key)) { - style.removeProperty(key); - } else if (key === 'float') { - style.cssFloat = ''; - } else { - style[key] = ''; - } - } - } - - // Set new or changed styles - for (const key in newStyle) { - const prevValue = prevStyle[key]; - const newValue = newStyle[key]; - // eslint-disable-next-line no-prototype-builtins - if (newStyle.hasOwnProperty(key) && newValue !== prevValue) { - if (newValue == null || newValue === '' || typeof newValue === 'boolean') { - if (isCustomProperty(key)) { - style.removeProperty(key); - } else if (key === 'float') { - style.cssFloat = ''; - } else { - style[key] = ''; - } - } else if (isCustomProperty(key)) { - style.setProperty(key, newStyle); - } else if (key === 'float') { - style.cssFloat = newStyle; - } else if (typeof newValue === 'number') { - el.style[key] = newValue + 'px'; - } else { - el.style[key] = newValue; - } - } - } - - // Store the new style for future comparisons - el._prevStyle = { ...newStyle }; -} - -/** - * @brief Plainly set dataset - * @param el - * @param value - */ -export function setDataset(el, value) { - Object.assign(el.dataset, value); -} - -/** - * @brief Set HTML property with checking value equality first - * @param el - * @param key - * @param value - */ -export function setHTMLProp(el, key, valueFunc, deps) { - // ---- Comparing deps, same value won't trigger - // will lead to a bug if the value is set outside of the DLNode - // e.g. setHTMLProp(el, "textContent", "value", []) - // => el.textContent = "other" - // => setHTMLProp(el, "textContent", "value", []) - // The value will be set to "other" instead of "value" - if (cache(el, key, deps)) return; - el[key] = valueFunc(); -} - -/** - * @brief Plainly set HTML properties - * @param el - * @param value - */ -export function setHTMLProps(el, value) { - Object.entries(value).forEach(([key, v]) => { - if (key === 'style') return setStyle(el, v); - if (key === 'dataset') return setDataset(el, v); - setHTMLProp(el, key, () => v, []); - }); -} - -/** - * @brief Set HTML attribute with checking value equality first - * @param el - * @param key - * @param value - */ -export function setHTMLAttr(el, key, valueFunc, deps) { - if (cache(el, key, deps)) return; - el.setAttribute(key, valueFunc()); -} - -/** - * @brief Plainly set HTML attributes - * @param el - * @param value - */ -export function setHTMLAttrs(el, value) { - Object.entries(value).forEach(([key, v]) => { - setHTMLAttr(el, key, () => v, []); - }); -} - -/** - * @brief Set memorized event, store the previous event in el[`$on${key}`], if it exists, remove it first - * @param el - * @param key - * @param value - */ -export function setEvent(el, key, value) { - const prevEvent = el[`$on${key}`]; - if (prevEvent) el.removeEventListener(key, prevEvent); - el.addEventListener(key, value); - el[`$on${key}`] = value; -} - -function eventHandler(e) { - const key = `$$${e.type}`; - for (const node of e.composedPath()) { - if (node[key]) node[key](e); - if (e.cancelBubble) return; - } -} - -export function delegateEvent(el, key, value) { - if (el[`$$${key}`] === value) return; - el[`$$${key}`] = value; - if (!DLStore.delegatedEvents.has(key)) { - DLStore.delegatedEvents.add(key); - DLStore.document.addEventListener(key, eventHandler); - } -} - -/** - * @brief Shortcut for document.createElement - * @param tag - * @returns HTMLElement - */ -export function createElement(tag) { - return DLStore.document.createElement(tag); -} - -/** - * @brief Insert any DLNode into an element, set the _$nodes and append the element to the element's children - * @param el - * @param node - * @param position - */ -export function insertNode(el, node, position) { - // ---- Set _$nodes - if (!el._$nodes) el._$nodes = Array.from(el.childNodes); - el._$nodes.splice(position, 0, node); - - // ---- Insert nodes' elements - const flowIdx = DLNode.getFlowIndexFromNodes(el._$nodes, node); - DLNode.appendNodesWithIndex([node], el, flowIdx); - // ---- Set parentEl - DLNode.addParentEl([node], el); -} - -export function appendNode(el, child) { - // ---- Set _$nodes - if (!el._$nodes) el._$nodes = Array.from(el.childNodes); - el._$nodes.push(child); - - el.appendChild(child); -} -/** - * @brief An inclusive assign prop function that accepts any type of prop - * @param el - * @param key - * @param value - */ -export function forwardHTMLProp(el, key, valueFunc, deps) { - if (key === 'style') return setStyle(el, valueFunc()); - if (key === 'dataset') return setDataset(el, valueFunc()); - if (key === 'element') return; - if (key === 'prop') return setHTMLProps(el, valueFunc()); - if (key === 'attr') return setHTMLAttrs(el, valueFunc()); - if (key === 'innerHTML') return setHTMLProp(el, 'innerHTML', valueFunc, deps); - if (key === 'textContent') return setHTMLProp(el, 'textContent', valueFunc, deps); - if (key === 'forwardProp') return; - if (key.startsWith('on')) { - return setEvent(el, key.slice(2).toLowerCase(), valueFunc()); - } - setHTMLAttr(el, key, valueFunc, deps); -} +import { getFlowIndexFromNodes, appendNodesWithIndex, addParentEl } from '../InulaNode'; +import { equal } from '../equal'; +import { InulaHTMLNode, TextNode, InulaNode } from '../types'; + +const delegatedEvents = new Set(); + +/** + * @brief Shortcut for document.createElement + * @param tag + * @returns HTMLElement + */ +export function createElement(tag: string): HTMLElement { + return document.createElement(tag); +} + +/** + * @brief Shorten document.createTextNode + * @param value + * @returns Text + */ +export function createTextNode(value: string, deps?: unknown[]) { + const node = document.createTextNode(value) as unknown as TextNode; + if (deps) node.deps = deps; + return node; +} + +/** + * @brief Update text node and check if the value is changed + * @param node + * @param value + */ +export function updateText(node: TextNode, valueFunc: () => string, deps: unknown[]) { + if (equal(deps, node.deps)) return; + const value = valueFunc(); + node.textContent = value; + node.deps = deps; +} + +function cache(el: HTMLElement, key: string, deps: any[]): boolean { + if (deps.length === 0) return false; + const cacheKey = `$${key}`; + if (equal(deps, (el as any)[cacheKey])) return true; + (el as any)[cacheKey] = deps; + return false; +} + +const isCustomProperty = (name: string): boolean => name.startsWith('--'); + +interface StyleObject { + [key: string]: string | number | null | undefined; +} + +export function setStyle(el: InulaHTMLNode, newStyle: CSSStyleDeclaration): void { + const style = el.style; + const prevStyle = el._prevStyle || {}; + + // Remove styles that are no longer present + for (const key in prevStyle) { + // eslint-disable-next-line no-prototype-builtins + if (prevStyle.hasOwnProperty(key) && (newStyle == null || !newStyle.hasOwnProperty(key))) { + if (isCustomProperty(key)) { + style.removeProperty(key); + } else if (key === 'float') { + style.cssFloat = ''; + } else { + style[key] = ''; + } + } + } + + // Set new or changed styles + for (const key in newStyle) { + const prevValue = prevStyle[key]; + const newValue = newStyle[key]; + // eslint-disable-next-line no-prototype-builtins + if (newStyle.hasOwnProperty(key) && newValue !== prevValue) { + if (newValue == null || newValue === '' || typeof newValue === 'boolean') { + if (isCustomProperty(key)) { + style.removeProperty(key); + } else if (key === 'float') { + style.cssFloat = ''; + } else { + style[key] = ''; + } + } else if (isCustomProperty(key)) { + style.setProperty(key, newValue); + } else if (key === 'float') { + style.cssFloat = newValue; + } else { + el.style[key] = newValue; + } + } + } + + // Store the new style for future comparisons + el._prevStyle = { ...newStyle }; +} + +/** + * @brief Plainly set dataset + * @param el + * @param value + */ + +export function setDataset(el: HTMLElement, value: { [key: string]: string }): void { + Object.assign(el.dataset, value); +} + +/** + * @brief Set HTML property with checking value equality first + */ + +export function setHTMLProp(el: HTMLElement, key: string, valueFunc: () => any, deps: any[]): void { + // ---- Comparing deps, same value won't trigger + // will lead to a bug if the value is set outside of the DLNode + // e.g. setHTMLProp(el, "textContent", "value", []) + // => el.textContent = "other" + // => setHTMLProp(el, "textContent", "value", []) + // The value will be set to "other" instead of "value" + if (cache(el, key, deps)) return; + (el as any)[key] = valueFunc(); +} + +/** + * @brief Plainly set HTML properties + */ + +export function setHTMLProps(el: InulaHTMLNode, value: HTMLAttrsObject): void { + Object.entries(value).forEach(([key, v]) => { + if (key === 'style') return setStyle(el, v as CSSStyleDeclaration); + if (key === 'dataset') return setDataset(el, v as { [key: string]: string }); + setHTMLProp(el, key, () => v, []); + }); +} + +/** + * @brief Set HTML attribute with checking value equality first + * @param el + * @param key + * @param valueFunc + * @param deps + */ + +export function setHTMLAttr(el: InulaHTMLNode, key: string, valueFunc: () => string, deps: any[]): void { + if (cache(el, key, deps)) return; + el.setAttribute(key, valueFunc()); +} + +interface HTMLAttrsObject { + [key: string]: string | number | boolean | object | undefined; + style?: CSSStyleDeclaration; + dataset?: { [key: string]: string }; +} + +/** + * @brief Plainly set HTML attributes + * @param el + * @param value + */ + +export function setHTMLAttrs(el: InulaHTMLNode, value: HTMLAttrsObject): void { + Object.entries(value).forEach(([key, v]) => { + setHTMLAttr(el, key, () => v, []); + }); +} + +/** + * @brief Set memorized event, store the previous event in el[`$on${key}`], if it exists, remove it first + * @param el + * @param key + * @param value + */ + +export function setEvent(el: InulaHTMLNode, key: string, value: EventListener): void { + const prevEvent = el[`$on${key}`]; + if (prevEvent) el.removeEventListener(key, prevEvent); + el.addEventListener(key, value); + el[`$on${key}`] = value; +} + +function eventHandler(e: Event): void { + const key = `$$${e.type}`; + for (const node of e.composedPath()) { + if (node[key]) node[key](e); + if (e.cancelBubble) return; + } +} + +export function delegateEvent(el: InulaHTMLNode, key: string, value: EventListener): void { + if (el[`$$${key}`] === value) return; + el[`$$${key}`] = value; + if (!delegatedEvents.has(key)) { + delegatedEvents.add(key); + document.addEventListener(key, eventHandler); + } +} + +export function appendNode(el: InulaHTMLNode, child: InulaHTMLNode) { + // ---- Set _$nodes + if (!el._$nodes) el._$nodes = Array.from(el.childNodes) as InulaHTMLNode[]; + el._$nodes.push(child); + + el.appendChild(child); +} + +/** + * @brief Insert any DLNode into an element, set the _$nodes and append the element to the element's children + * @param el + * @param node + * @param position + */ +export function insertNode(el: InulaHTMLNode, node: InulaNode, position: number): void { + // ---- Set _$nodes + if (!el._$nodes) el._$nodes = Array.from(el.childNodes) as InulaHTMLNode[]; + el._$nodes.splice(position, 0, node); + + // ---- Insert nodes' elements + const flowIdx = getFlowIndexFromNodes(el._$nodes, node); + appendNodesWithIndex([node], el, flowIdx); + // ---- Set parentEl + addParentEl([node], el); +} diff --git a/packages/inula-next/src/scheduler.js b/packages/inula-next/src/scheduler.ts similarity index 90% rename from packages/inula-next/src/scheduler.js rename to packages/inula-next/src/scheduler.ts index 2b454b4c926785ca25697ab652cc7c98bc37d7d9..73d900ca21eb9bc8a9c65a781f1f6951f7855423 100644 --- a/packages/inula-next/src/scheduler.js +++ b/packages/inula-next/src/scheduler.ts @@ -1,25 +1,23 @@ -/* - * 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. - */ - -const p = Promise.resolve(); - -/** - * Schedule a task to run in the next microtask. - * - * @param {() => void} task - */ -export function schedule(task) { - p.then(task); -} +/* + * 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. + */ + +const p = Promise.resolve(); + +/** + * Schedule a task to run in the next microtask. + */ +export function schedule(task: () => void) { + p.then(task); +} diff --git a/packages/inula-next/src/store.js b/packages/inula-next/src/store.js deleted file mode 100644 index ae038bf453aa082979008220a56100b43a051413..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/store.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Store } from '@openinula/store'; - -// ---- Using external Store to store global and document -// Because Store is a singleton, it is safe to use it as a global variable -// If created in Inula-Next package, different package versions will introduce -// multiple Store instances. - -if (!('global' in Store)) { - if (typeof window !== 'undefined') { - Store.global = window; - } else if (typeof global !== 'undefined') { - Store.global = global; - } else { - Store.global = {}; - } -} -if (!('document' in Store)) { - if (typeof document !== 'undefined') { - Store.document = document; - } -} - -export const DLStore = { ...Store, delegatedEvents: new Set() }; - -export function setGlobal(globalObj) { - DLStore.global = globalObj; -} - -export function setDocument(customDocument) { - DLStore.document = customDocument; -} - -/** - * @brief Compare the deps with the previous deps - * @param deps - * @param prevDeps - * @returns - */ -export function cached(deps, prevDeps) { - if (!prevDeps || deps.length !== prevDeps.length) return false; - return deps.every((dep, i) => !(dep instanceof Object) && prevDeps[i] === dep); -} diff --git a/packages/inula-next/src/types.ts b/packages/inula-next/src/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..da44a7667ecf48f2126315c3a198bc789325b92f --- /dev/null +++ b/packages/inula-next/src/types.ts @@ -0,0 +1,145 @@ +/* + * 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 { InulaNodeType } from '@openinula/next-shared'; + +export type Lifecycle = () => void; +export type ScopedLifecycle = Lifecycle[]; + +export { type Properties as CSSProperties } from 'csstype'; + +export type InulaNode = VNode | TextNode | InulaHTMLNode; + +export interface VNode { + __type: InulaNodeType; + _$nodes: InulaNode[]; + _$parentEl?: InulaHTMLNode; + $nonkeyedCache?: Record; +} + +export interface TextNode extends Text { + deps: unknown[]; +} + +export interface InulaHTMLNode extends HTMLElement { + _$nodes: InulaNode[]; + _prevStyle?: CSSStyleDeclaration; + [key: `$on${string}`]: EventListener + [key: `$$${string}`]: EventListener +} + +export interface ComposableNode = Record> extends VNode { + __type: InulaNodeType; + parent?: ComposableNode; + props: Props; + cache?: Record; + _$nodes: InulaHTMLNode[]; + mounting?: boolean; + _$unmounted?: boolean; + _$forwardPropsSet?: Set; + _$forwardPropsId?: string[]; + _$contentKey?: string; + _$depNumsToUpdate?: number[]; + updateState: (bit: number) => void; + updateProp: (...args: any[]) => void; + updateContext?: (context: any, key: string, value: any) => void; + getUpdateViews?: () => any; + didUnmount?: () => void; + willUnmount?: () => void; + didMount?: () => void; + updateView?: (depNum: number) => void; +} + +export interface CompNode extends ComposableNode { + __type: InulaNodeType.Comp; +} + +export interface HookNode extends ComposableNode { + __type: InulaNodeType.Hook; + bitmap: number; + parent: HookNode | CompNode; + value?: () => unknown; +} + +/** + * @brief Mutable node is a node that this._$nodes can be changed, things need to pay attention: + * 1. The context of the new nodes should be the same as the old nodes + * 2. The new nodes should be added to the parentEl + * 3. The old nodes should be removed from the parentEl + */ +export interface MutableNode extends VNode { + willUnmountFuncs: UnmountShape; + didUnmountFuncs: UnmountShape; + savedContextNodes: Map> | null; +} + +export interface CondNode extends MutableNode { + cond: number; + didntChange: boolean; + __type: InulaNodeType.Cond; + depNum: number; + condFunc: (condNode: CondNode) => VNode[]; + /** + * @brief assigned by condNode.condFunc in compile time + * @param changed + * @returns + */ + updateFunc?: (changed: number) => void; + _$parentEl?: InulaHTMLNode; +} + +export interface ExpNode extends MutableNode { + __type: InulaNodeType.Exp; + _$nodes: InulaNode[]; + _$parentEl?: InulaHTMLNode; + deps: unknown[]; +} + +export interface ForNode extends MutableNode>, VNode { + array: T[]; + __type: InulaNodeType.For; + depNum: number; + keys: number[]; + nodeFunc: (item: T, idx: number, updateArr: any[]) => VNode[]; + _$nodes: InulaNode[]; + _$parentEl?: InulaHTMLNode; + nodesMap: Map; + updateArr: any[]; +} + +export interface Context | null> { + id: symbol; + value: V | null; +} + +export interface ContextNode> { + value: V; + context: Context; + __type: InulaNodeType.Context; + depMap: Record>; + _$nodes: VNode[]; + _$unmounted?: boolean; + consumers: Set; + prevValue?: V | null; + prevContextNode?: ContextNode | null; +} + +export type Updater = (changed: number) => void; + +export interface ChildrenNode { + __type: InulaNodeType.Children; + childrenFunc: (addUpdate: (updater: Updater) => void) => VNode[]; + updaters: Set; +} diff --git a/packages/inula-next/src/types/index.d.ts b/packages/inula-next/src/types/index.d.ts deleted file mode 100644 index f58cb2facacccbdfa830ff5fb0fff65547977d7a..0000000000000000000000000000000000000000 --- a/packages/inula-next/src/types/index.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -export { type Properties as CSSProperties } from 'csstype'; - -// ---- With actual value -export function render(DL: any, idOrEl: string | HTMLElement): void; - -export function willMount(fn: () => void): void; -export function didMount(fn: () => void): void; -export function willUnmount(fn: () => void): void; -export function didUnmount(fn: () => void): void; -interface Context { - value: V; -} - -export function createContext(defaultVal: T): Context; -export function useContext(ctx: Context): T; - -export const View: any; -export const update: any; diff --git a/packages/inula-next/test/components.test.tsx b/packages/inula-next/test/components.test.tsx index 61456d90e7813ae3a19d241963c86ad3ad6efdbc..b95c947a19ad505697a169832dd963596f74a5ad 100644 --- a/packages/inula-next/test/components.test.tsx +++ b/packages/inula-next/test/components.test.tsx @@ -1,247 +1,247 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render } from '../src'; - -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); - -describe('components', () => { - describe('ref', () => { - it('should support ref', ({ container }) => { - let ref: HTMLElement; - - function App() { - let count = 0; - let _ref: HTMLElement; - - didMount(() => { - ref = _ref; - }); - - return
test
; - } - - render(App, container); - - expect(ref).toBeInstanceOf(HTMLElement); - }); - - it('should support ref forwarding', ({ container }) => { - let ref: HTMLElement; - - function App() { - let count = 0; - let _ref: HTMLElement; - - didMount(() => { - ref = _ref; - }); - - return test; - } - - function Input({ ref }) { - return ; - } - - render(App, container); - expect(ref).toBeInstanceOf(HTMLInputElement); - }); - - it('should support ref with function', ({ container }) => { - const fn = vi.fn(); - - function App() { - const ref = (el: HTMLElement) => { - fn(); - expect(el).toBeInstanceOf(HTMLElement); - }; - - return
test
; - } - - render(App, container); - expect(fn).toHaveBeenCalledTimes(1); - }); - - it('should support comp ref exposing', ({ container }) => { - let ref: HTMLElement; - const fn = vi.fn(); - - function App() { - let count = 0; - let _ref; - didMount(() => { - ref = _ref; - _ref.fn(); - }); - return test; - } - - function Input({ ref }) { - let input; - didMount(() => { - ref({ fn, input }); - }); - return ; - } - - render(App, container); - expect(fn).toHaveBeenCalledTimes(1); - expect(ref.input).toBeInstanceOf(HTMLInputElement); - }); - }); - - describe('composition', () => { - it('should update prop', ({ container }) => { - let update: (name: string) => void; - - function App() { - let name = 'child'; - update = (val: string) => { - name = val; - }; - return ; - } - - function Child({ name }: { name: string }) { - return
name is {name}
; - } - - render(App, container); - expect(container.innerHTML).toBe('
name is child
'); - update('new'); - expect(container.innerHTML).toBe('
name is new
'); - }); - }); - describe('nested component', () => { - it('should render sub component using parent state', ({ container }) => { - function App() { - let count = 0; - - function Heading() { - return

{count}

; - } - - return ; - } - - render(App, container); - expect(container.innerHTML).toBe('

0

'); - }); - - it('should update nested component when parent state changes', ({ container }) => { - let setCount: (n: number) => void; - - function App() { - let count = 0; - setCount = (n: number) => { - count = n; - }; - - function Counter() { - return
Count: {count}
; - } - - return ; - } - - render(App, container); - expect(container.innerHTML).toBe('
Count: 0
'); - - setCount(5); - expect(container.innerHTML).toBe('
Count: 5
'); - }); - - it('should pass props through multiple levels of nesting', ({ container }) => { - function App() { - let name = 'Alice'; - - function Parent({ children }) { - return ( -
-

Parent

- {children} -
- ); - } - - function Child({ name }: { name: string }) { - return
Hello, {name}!
; - } - - return ( - - - - ); - } - - render(App, container); - expect(container.innerHTML).toBe( - '

Parent

Hello, Alice!
' - ); - }); - - it('should handle sibling nested components with independent state', ({ container }) => { - let incrementA: () => void; - let incrementB: () => void; - - function App() { - let a = 0; - let b = 0; - incrementA = () => { - a += 1; - }; - incrementB = () => { - b += 1; - }; - - function Counter({ name }: { name: string }) { - const value = name === 'A' ? a : b; - return ( -
- Counter {name}: {value} -
- ); - } - - return ( -
- - -
- ); - } - - render(App, container); - expect(container.innerHTML).toBe('
Counter A: 0
Counter B: 0
'); - - incrementA(); - expect(container.innerHTML).toBe('
Counter A: 1
Counter B: 0
'); - - incrementB(); - expect(container.innerHTML).toBe('
Counter A: 1
Counter B: 1
'); - }); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render } from '../src'; + +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); + +describe('components', () => { + describe('ref', () => { + it('should support ref', ({ container }) => { + let ref: HTMLElement; + + function App() { + let count = 0; + let _ref: HTMLElement; + + didMount(() => { + ref = _ref; + }); + + return
test
; + } + + render(App, container); + + expect(ref).toBeInstanceOf(HTMLElement); + }); + + it('should support ref forwarding', ({ container }) => { + let ref: HTMLElement; + + function App() { + let count = 0; + let _ref: HTMLElement; + + didMount(() => { + ref = _ref; + }); + + return test; + } + + function Input({ ref }) { + return ; + } + + render(App, container); + expect(ref).toBeInstanceOf(HTMLInputElement); + }); + + it('should support ref with function', ({ container }) => { + const fn = vi.fn(); + + function App() { + const ref = (el: HTMLElement) => { + fn(); + expect(el).toBeInstanceOf(HTMLElement); + }; + + return
test
; + } + + render(App, container); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should support comp ref exposing', ({ container }) => { + let ref: HTMLElement; + const fn = vi.fn(); + + function App() { + let count = 0; + let _ref; + didMount(() => { + ref = _ref; + _ref.fn(); + }); + return test; + } + + function Input({ ref }) { + let input; + didMount(() => { + ref({ fn, input }); + }); + return ; + } + + render(App, container); + expect(fn).toHaveBeenCalledTimes(1); + expect(ref.input).toBeInstanceOf(HTMLInputElement); + }); + }); + + describe('composition', () => { + it('should update prop', ({ container }) => { + let update: (name: string) => void; + + function App() { + let name = 'child'; + update = (val: string) => { + name = val; + }; + return ; + } + + function Child({ name }: { name: string }) { + return
name is {name}
; + } + + render(App, container); + expect(container.innerHTML).toBe('
name is child
'); + update('new'); + expect(container.innerHTML).toBe('
name is new
'); + }); + }); + describe('nested component', () => { + it('should render sub component using parent state', ({ container }) => { + function App() { + let count = 0; + + function Heading() { + return

{count}

; + } + + return ; + } + + render(App, container); + expect(container.innerHTML).toBe('

0

'); + }); + + it('should update nested component when parent state changes', ({ container }) => { + let setCount: (n: number) => void; + + function App() { + let count = 0; + setCount = (n: number) => { + count = n; + }; + + function Counter() { + return
Count: {count}
; + } + + return ; + } + + render(App, container); + expect(container.innerHTML).toBe('
Count: 0
'); + + setCount(5); + expect(container.innerHTML).toBe('
Count: 5
'); + }); + + it('should pass props through multiple levels of nesting', ({ container }) => { + function App() { + let name = 'Alice'; + + function Parent({ children }) { + return ( +
+

Parent

+ {children} +
+ ); + } + + function Child({ name }: { name: string }) { + return
Hello, {name}!
; + } + + return ( + + + + ); + } + + render(App, container); + expect(container.innerHTML).toBe( + '

Parent

Hello, Alice!
' + ); + }); + + it('should handle sibling nested components with independent state', ({ container }) => { + let incrementA: () => void; + let incrementB: () => void; + + function App() { + let a = 0; + let b = 0; + incrementA = () => { + a += 1; + }; + incrementB = () => { + b += 1; + }; + + function Counter({ name }: { name: string }) { + const value = name === 'A' ? a : b; + return ( +
+ Counter {name}: {value} +
+ ); + } + + return ( +
+ + +
+ ); + } + + render(App, container); + expect(container.innerHTML).toBe('
Counter A: 0
Counter B: 0
'); + + incrementA(); + expect(container.innerHTML).toBe('
Counter A: 1
Counter B: 0
'); + + incrementB(); + expect(container.innerHTML).toBe('
Counter A: 1
Counter B: 1
'); + }); + }); +}); diff --git a/packages/inula-next/test/computed.test.tsx b/packages/inula-next/test/computed.test.tsx index 8e229e815779521a552d8e6a5cd084a594a07778..293dbcd235c59554322c9aefe3f75048512aee7e 100644 --- a/packages/inula-next/test/computed.test.tsx +++ b/packages/inula-next/test/computed.test.tsx @@ -1,805 +1,820 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render, didMount } from '../src'; - -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); - -describe('Computed Properties', () => { - describe('Basic Functionality', () => { - it('should correctly compute a value based on a single dependency', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let count = 0; - const doubleCount = count * 2; - - function onClick() { - count = count + 1; - } - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - return ( -
-

{doubleCount}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('0'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('2'); - }); - - it('should update computed value when dependency changes', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let width = 5; - let height = 10; - const area = width * height; - - function onClick() { - width = width + 1; - } - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - return ( -
-

{area}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('50'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('60'); - }); - - it('Should correctly compute and render a derived string state', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let firstName = 'John'; - let lastName = 'Doe'; - const fullName = `${firstName} ${lastName}`; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function updateName() { - firstName = 'Jane'; - } - - return ( -
-

{fullName}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('John Doe'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Jane Doe'); - }); - - it('Should correctly compute and render a derived number state', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let price = 10; - let quantity = 2; - const total = price * quantity; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function increaseQuantity() { - quantity += 1; - } - - return ( -
-

Total: ${total}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('Total: $20'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Total: $30'); - }); - - it('Should correctly compute and render a derived boolean state', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let age = 17; - const isAdult = age >= 18; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function increaseAge() { - age += 1; - } - - return ( -
-

Is Adult: {isAdult ? 'Yes' : 'No'}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('Is Adult: No'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Is Adult: Yes'); - }); - - it('Should correctly compute and render a derived array state', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let numbers = [1, 2, 3, 4, 5]; - const evenNumbers = numbers.filter(n => n % 2 === 0); - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function addNumber() { - numbers.push(6); - } - - return ( -
-

Even numbers: {evenNumbers.join(', ')}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('Even numbers: 2, 4'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Even numbers: 2, 4, 6'); - }); - - it('Should correctly compute and render a derived object state', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let user = { name: 'John', age: 30 }; - const userSummary = { ...user, isAdult: user.age >= 18 }; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function updateAge() { - user.age = 17; - } - - return ( -
-

- {userSummary.name} is {userSummary.isAdult ? 'an adult' : 'not an adult'} -

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('John is an adult'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('John is not an adult'); - }); - - it('Should correctly compute state based on array index', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let items = ['Apple', 'Banana', 'Cherry']; - let index = 0; - const currentItem = items[index]; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function nextItem() { - index = (index + 1) % items.length; - } - - return ( -
-

Current item: {currentItem}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('Current item: Apple'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Current item: Banana'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Current item: Cherry'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Current item: Apple'); - }); - }); - - describe('Multiple Dependencies', () => { - it('should compute correctly with multiple dependencies', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let x = 5; - let y = 10; - let z = 2; - const result = x * y + z; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - return

{result}

; - } - - render(App, container); - - expect(resultElement.textContent).toBe('52'); - }); - - it('should update when any dependency changes', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let x = 5; - let y = 10; - const sum = x + y; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function onClickX() { - x = x + 1; - } - - function onClickY() { - y = y + 1; - } - - return ( -
-

{sum}

- - -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('15'); - container.querySelectorAll('button')[0].click(); - expect(resultElement.textContent).toBe('16'); - container.querySelectorAll('button')[1].click(); - expect(resultElement.textContent).toBe('17'); - }); - it('Should correctly compute and render a derived string state from multi dependency', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let firstName = 'John'; - let lastName = 'Doe'; - let title = 'Mr.'; - const fullName = `${title} ${firstName} ${lastName}`; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function updateName() { - firstName = 'Jane'; - title = 'Ms.'; - } - - return ( -
-

{fullName}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('Mr. John Doe'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Ms. Jane Doe'); - }); - - it('Should correctly compute and render a derived number state from multi dependency', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let length = 5; - let width = 3; - let height = 2; - const volume = length * width * height; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function updateDimensions() { - length += 1; - width += 2; - } - - return ( -
-

Volume: {volume}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('Volume: 30'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Volume: 60'); - }); - - it('Should correctly compute and render a derived boolean state from multi dependency', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let age = 20; - let hasLicense = false; - let hasCar = true; - const canDrive = age >= 18 && hasLicense && hasCar; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function updateStatus() { - hasLicense = true; - } - - return ( -
-

Can Drive: {canDrive ? 'Yes' : 'No'}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('Can Drive: No'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Can Drive: Yes'); - }); - - it('Should correctly compute and render a derived array state from multi dependency', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let numbers1 = [1, 2, 3]; - let numbers2 = [4, 5, 6]; - let filterEven = true; - const result = [...numbers1, ...numbers2].filter(n => (filterEven ? n % 2 === 0 : n % 2 !== 0)); - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function toggleFilter() { - filterEven = !filterEven; - } - - return ( -
-

Filtered numbers: {result.join(', ')}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('Filtered numbers: 2, 4, 6'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Filtered numbers: 1, 3, 5'); - }); - - it('Should correctly compute and render a derived object state from multi dependency', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let user = { name: 'John', age: 30 }; - let settings = { theme: 'dark', fontSize: 14 }; - let isLoggedIn = true; - const userProfile = { - ...user, - ...settings, - status: isLoggedIn ? 'Online' : 'Offline', - }; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function updateStatus() { - isLoggedIn = false; - settings.theme = 'light'; - } - - return ( -
-

- {userProfile.name} ({userProfile.age}) - {userProfile.status} - Theme: {userProfile.theme} -

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('John (30) - Online - Theme: dark'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('John (30) - Offline - Theme: light'); - }); - }); - - describe('Advanced Computed States', () => { - it('Should support basic arithmetic operations', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let a = 10; - let b = 5; - const sum = a + b; - const difference = a - b; - const product = a * b; - const quotient = a / b; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - return ( -
-

Sum: {sum}

-

Difference: {difference}

-

Product: {product}

-

Quotient: {quotient}

-
- ); - } - - render(App, container); - - expect(resultElement.innerHTML).toBe('

Sum: 15

Difference: 5

Product: 50

Quotient: 2

'); - }); - - it('Should support array indexing', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let arr = [10, 20, 30, 40, 50]; - let index = 2; - const value = arr[index]; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function updateIndex() { - index = 4; - } - - return ( -
-

Value: {value}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('Value: 30'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Value: 50'); - }); - - it('Should support property access', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let obj = { name: 'John', age: 30 }; - const name = obj.name; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function updateName() { - obj.name = 'Jane'; - } - - return ( -
-

Name: {name}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('Name: John'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Name: Jane'); - }); - - it('Should support function calls', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let numbers = [1, 2, 3, 4, 5]; - const sum = numbers.reduce((a, b) => a + b, 0); - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - return

Sum: {sum}

; - } - - render(App, container); - - expect(resultElement.textContent).toBe('Sum: 15'); - }); - - it('Should support various number operations', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let num = 3.14159; - const rounded = Math.round(num); - const floored = Math.floor(num); - const ceiled = Math.ceil(num); - const squared = Math.pow(num, 2); - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - return ( -
-

Rounded: {rounded}

-

Floored: {floored}

-

Ceiled: {ceiled}

-

Squared: {squared.toFixed(2)}

-
- ); - } - - render(App, container); - - expect(resultElement.innerHTML).toBe('

Rounded: 3

Floored: 3

Ceiled: 4

Squared: 9.87

'); - }); - - it('Should support map operations', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let numbers = [1, 2, 3, 4, 5]; - const squaredNumbers = numbers.map(n => n * n); - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - return

Squared: {squaredNumbers.join(', ')}

; - } - - render(App, container); - - expect(resultElement.textContent).toBe('Squared: 1, 4, 9, 16, 25'); - }); - - it('Should support conditional expressions', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let age = 20; - let hasLicense = true; - const canDrive = age >= 18 ? (hasLicense ? 'Yes' : 'No, needs license') : 'No, too young'; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function updateAge() { - age = 16; - } - - return ( -
-

Can Drive: {canDrive}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('Can Drive: Yes'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('Can Drive: No, too young'); - }); - }); - - describe('Nested Computed Properties', () => { - it('should handle nested computed properties correctly', ({ container }) => { - let resultElement: HTMLElement; - - function App() { - let base = 5; - const square = base * base; - const cube = square * base; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - }); - - function onClick() { - base = base + 1; - } - - return ( -
-

{cube}

- -
- ); - } - - render(App, container); - - expect(resultElement.textContent).toBe('125'); - container.querySelector('button')!.click(); - expect(resultElement.textContent).toBe('216'); - }); - }); - - describe('Conditional Dependence', () => { - it('should handle conditional dependencies correctly', ({ container }) => { - let resultElement: HTMLElement; - let toggleButton: HTMLElement; - let incrementValue1Button: HTMLElement; - let incrementValue2Button: HTMLElement; - - function App() { - let useAlternative = false; - let value1 = 10; - let value2 = 20; - - // This computed property has conditional dependencies - const result = useAlternative ? value2 * 2 : value1 * 2; - - didMount(() => { - resultElement = container.querySelector('[data-testid="result"]')!; - toggleButton = container.querySelector('[data-testid="toggle"]')!; - incrementValue1Button = container.querySelector('[data-testid="increment-value1"]')!; - incrementValue2Button = container.querySelector('[data-testid="increment-value2"]')!; - }); - - function toggle() { - useAlternative = !useAlternative; - } - - const onClick1 = () => { - value1 += 5; - }; - - const onClick2 = () => { - value2 += 5; - }; - return ( -
-

{result}

- - - -
- ); - } - - render(App, container); - - // Check initial state - expect(resultElement.textContent).toBe('20'); - - // Switch to alternative value - toggleButton.click(); - expect(resultElement.textContent).toBe('40'); - - // Increment value1 (should not affect result as we're using value2) - incrementValue1Button.click(); - expect(resultElement.textContent).toBe('40'); - - // Increment value2 (should affect result) - incrementValue2Button.click(); - expect(resultElement.textContent).toBe('50'); - - // Switch back to original value - toggleButton.click(); - expect(resultElement.textContent).toBe('30'); // value1 was incremented earlier - }); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render, didMount } from '../src'; + +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); + +describe('Computed Properties', () => { + describe('Basic Functionality', () => { + it('should correctly compute a value based on a single dependency', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let count = 0; + const doubleCount = count * 2; + + function onClick() { + count = count + 1; + } + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + return ( +
+

{doubleCount}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('0'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('2'); + }); + + it('should update computed value when dependency changes', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let width = 5; + let height = 10; + const area = width * height; + + function onClick() { + width = width + 1; + } + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + return ( +
+

{area}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('50'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('60'); + }); + + it('Should correctly compute and render a derived string state', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let firstName = 'John'; + let lastName = 'Doe'; + const fullName = `${firstName} ${lastName}`; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateName() { + firstName = 'Jane'; + } + + return ( +
+

{fullName}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('John Doe'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Jane Doe'); + }); + + it('Should correctly compute and render a derived number state', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let price = 10; + let quantity = 2; + const total = price * quantity; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function increaseQuantity() { + quantity += 1; + } + + return ( +
+

Total: ${total}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Total: $20'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Total: $30'); + }); + + it('Should correctly compute and render a derived boolean state', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let age = 17; + const isAdult = age >= 18; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function increaseAge() { + age += 1; + } + + return ( +
+

Is Adult: {isAdult ? 'Yes' : 'No'}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Is Adult: No'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Is Adult: Yes'); + }); + + it('Should correctly compute and render a derived array state', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let numbers = [1, 2, 3, 4, 5]; + const evenNumbers = numbers.filter(n => n % 2 === 0); + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function addNumber() { + numbers.push(6); + } + + return ( +
+

Even numbers: {evenNumbers.join(', ')}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Even numbers: 2, 4'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Even numbers: 2, 4, 6'); + }); + + it('Should correctly compute and render a derived object state', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let user = { name: 'John', age: 30 }; + const userSummary = { ...user, isAdult: user.age >= 18 }; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateAge() { + user.age = 17; + } + + return ( +
+

+ {userSummary.name} is {userSummary.isAdult ? 'an adult' : 'not an adult'} +

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('John is an adult'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('John is not an adult'); + }); + + it('Should correctly compute state based on array index', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let items = ['Apple', 'Banana', 'Cherry']; + let index = 0; + const currentItem = items[index]; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function nextItem() { + index = (index + 1) % items.length; + } + + return ( +
+

Current item: {currentItem}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Current item: Apple'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Current item: Banana'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Current item: Cherry'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Current item: Apple'); + }); + }); + + describe('Multiple Dependencies', () => { + it('should compute correctly with multiple dependencies', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let x = 5; + let y = 10; + let z = 2; + const result = x * y + z; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + return

{result}

; + } + + render(App, container); + + expect(resultElement.textContent).toBe('52'); + }); + + it.fails('should compute correctly with functional dependencies', ({ container }) => { + function App() { + let x = 1; + const double = x * 2; + const quadruple = double * 2; + const getQuadruple = () => quadruple; + const result = getQuadruple() + x; + return
{result}
; + } + + render(App, container); + + expect(container.innerHTML).toBe('
5
'); + }); + + it('should update when any dependency changes', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let x = 5; + let y = 10; + const sum = x + y; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function onClickX() { + x = x + 1; + } + + function onClickY() { + y = y + 1; + } + + return ( +
+

{sum}

+ + +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('15'); + container.querySelectorAll('button')[0].click(); + expect(resultElement.textContent).toBe('16'); + container.querySelectorAll('button')[1].click(); + expect(resultElement.textContent).toBe('17'); + }); + it('Should correctly compute and render a derived string state from multi dependency', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let firstName = 'John'; + let lastName = 'Doe'; + let title = 'Mr.'; + const fullName = `${title} ${firstName} ${lastName}`; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateName() { + firstName = 'Jane'; + title = 'Ms.'; + } + + return ( +
+

{fullName}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Mr. John Doe'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Ms. Jane Doe'); + }); + + it('Should correctly compute and render a derived number state from multi dependency', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let length = 5; + let width = 3; + let height = 2; + const volume = length * width * height; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateDimensions() { + length += 1; + width += 2; + } + + return ( +
+

Volume: {volume}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Volume: 30'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Volume: 60'); + }); + + it('Should correctly compute and render a derived boolean state from multi dependency', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let age = 20; + let hasLicense = false; + let hasCar = true; + const canDrive = age >= 18 && hasLicense && hasCar; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateStatus() { + hasLicense = true; + } + + return ( +
+

Can Drive: {canDrive ? 'Yes' : 'No'}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Can Drive: No'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Can Drive: Yes'); + }); + + it('Should correctly compute and render a derived array state from multi dependency', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let numbers1 = [1, 2, 3]; + let numbers2 = [4, 5, 6]; + let filterEven = true; + const result = [...numbers1, ...numbers2].filter(n => (filterEven ? n % 2 === 0 : n % 2 !== 0)); + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function toggleFilter() { + filterEven = !filterEven; + } + + return ( +
+

Filtered numbers: {result.join(', ')}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Filtered numbers: 2, 4, 6'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Filtered numbers: 1, 3, 5'); + }); + + it('Should correctly compute and render a derived object state from multi dependency', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let user = { name: 'John', age: 30 }; + let settings = { theme: 'dark', fontSize: 14 }; + let isLoggedIn = true; + const userProfile = { + ...user, + ...settings, + status: isLoggedIn ? 'Online' : 'Offline', + }; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateStatus() { + isLoggedIn = false; + settings.theme = 'light'; + } + + return ( +
+

+ {userProfile.name} ({userProfile.age}) - {userProfile.status} - Theme: {userProfile.theme} +

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('John (30) - Online - Theme: dark'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('John (30) - Offline - Theme: light'); + }); + }); + + describe('Advanced Computed States', () => { + it('Should support basic arithmetic operations', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let a = 10; + let b = 5; + const sum = a + b; + const difference = a - b; + const product = a * b; + const quotient = a / b; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + return ( +
+

Sum: {sum}

+

Difference: {difference}

+

Product: {product}

+

Quotient: {quotient}

+
+ ); + } + + render(App, container); + + expect(resultElement.innerHTML).toBe('

Sum: 15

Difference: 5

Product: 50

Quotient: 2

'); + }); + + it('Should support array indexing', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let arr = [10, 20, 30, 40, 50]; + let index = 2; + const value = arr[index]; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateIndex() { + index = 4; + } + + return ( +
+

Value: {value}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Value: 30'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Value: 50'); + }); + + it('Should support property access', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let obj = { name: 'John', age: 30 }; + const name = obj.name; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateName() { + obj.name = 'Jane'; + } + + return ( +
+

Name: {name}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Name: John'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Name: Jane'); + }); + + it('Should support function calls', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let numbers = [1, 2, 3, 4, 5]; + const sum = numbers.reduce((a, b) => a + b, 0); + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + return

Sum: {sum}

; + } + + render(App, container); + + expect(resultElement.textContent).toBe('Sum: 15'); + }); + + it('Should support various number operations', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let num = 3.14159; + const rounded = Math.round(num); + const floored = Math.floor(num); + const ceiled = Math.ceil(num); + const squared = Math.pow(num, 2); + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + return ( +
+

Rounded: {rounded}

+

Floored: {floored}

+

Ceiled: {ceiled}

+

Squared: {squared.toFixed(2)}

+
+ ); + } + + render(App, container); + + expect(resultElement.innerHTML).toBe('

Rounded: 3

Floored: 3

Ceiled: 4

Squared: 9.87

'); + }); + + it('Should support map operations', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let numbers = [1, 2, 3, 4, 5]; + const squaredNumbers = numbers.map(n => n * n); + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + return

Squared: {squaredNumbers.join(', ')}

; + } + + render(App, container); + + expect(resultElement.textContent).toBe('Squared: 1, 4, 9, 16, 25'); + }); + + it('Should support conditional expressions', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let age = 20; + let hasLicense = true; + const canDrive = age >= 18 ? (hasLicense ? 'Yes' : 'No, needs license') : 'No, too young'; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateAge() { + age = 16; + } + + return ( +
+

Can Drive: {canDrive}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Can Drive: Yes'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Can Drive: No, too young'); + }); + }); + + describe('Nested Computed Properties', () => { + it('should handle nested computed properties correctly', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let base = 5; + const square = base * base; + const cube = square * base; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function onClick() { + base = base + 1; + } + + return ( +
+

{cube}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('125'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('216'); + }); + }); + + describe('Conditional Dependence', () => { + it('should handle conditional dependencies correctly', ({ container }) => { + let resultElement: HTMLElement; + let toggleButton: HTMLElement; + let incrementValue1Button: HTMLElement; + let incrementValue2Button: HTMLElement; + + function App() { + let useAlternative = false; + let value1 = 10; + let value2 = 20; + + // This computed property has conditional dependencies + const result = useAlternative ? value2 * 2 : value1 * 2; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + toggleButton = container.querySelector('[data-testid="toggle"]')!; + incrementValue1Button = container.querySelector('[data-testid="increment-value1"]')!; + incrementValue2Button = container.querySelector('[data-testid="increment-value2"]')!; + }); + + function toggle() { + useAlternative = !useAlternative; + } + + const onClick1 = () => { + value1 += 5; + }; + + const onClick2 = () => { + value2 += 5; + }; + return ( +
+

{result}

+ + + +
+ ); + } + + render(App, container); + + // Check initial state + expect(resultElement.textContent).toBe('20'); + + // Switch to alternative value + toggleButton.click(); + expect(resultElement.textContent).toBe('40'); + + // Increment value1 (should not affect result as we're using value2) + incrementValue1Button.click(); + expect(resultElement.textContent).toBe('40'); + + // Increment value2 (should affect result) + incrementValue2Button.click(); + expect(resultElement.textContent).toBe('50'); + + // Switch back to original value + toggleButton.click(); + expect(resultElement.textContent).toBe('30'); // value1 was incremented earlier + }); + }); +}); diff --git a/packages/inula-next/test/conditional.test.tsx b/packages/inula-next/test/conditional.test.tsx index 0724ddf657915780ae81336902eec442c3e2be4c..9c5085ef55887cd9eb403da19138853c438dc9be 100644 --- a/packages/inula-next/test/conditional.test.tsx +++ b/packages/inula-next/test/conditional.test.tsx @@ -1,265 +1,265 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render } from '../src'; - -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); - -describe('conditional rendering', () => { - it('should if, else, else if', ({ container }) => { - let set: (num: number) => void; - - function App() { - let count = 2; - set = (val: number) => { - count = val; - }; - return ( - <> - 1}>{count} is bigger than is 1 - {count} is equal to 1 - {count} is smaller than 1 - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('2 is bigger than is 1'); - set(1); - expect(container.innerHTML).toBe('1 is equal to 1'); - set(0); - expect(container.innerHTML).toBe('0 is smaller than 1'); - }); - - it('should support nested if', ({ container }) => { - let set: (num: number) => void; - - function App() { - let count = 0; - set = (val: number) => { - count = val; - }; - return ( - 1}> - {count} is bigger than is 1 - 2}> -
{count} is bigger than is 2
-
-
- ); - } - - render(App, container); - expect(container.innerHTML).toMatchInlineSnapshot(`""`); - set(2); - expect(container.innerHTML).toMatchInlineSnapshot(` - "2 is bigger than is 1" - `); - set(3); - expect(container.innerHTML).toMatchInlineSnapshot(` - "3 is bigger than is 1
3 is bigger than is 2
" - `); - set(2); - expect(container.innerHTML).toMatchInlineSnapshot(` - "2 is bigger than is 1" - `); - }); - it('should transform "and expression in and expression" to "if tag in if tag"', ({ container }) => { - let set: (num: number) => void; - - function MyComp() { - let count = 0; - set = (val: number) => { - count = val; - }; - return
{count > 0 && count > 1 &&

hello world {count}

}
; - } - - render(MyComp, container); - set(0); - expect(container.innerHTML).toMatchInlineSnapshot(`"
"`); - set(1); - expect(container.innerHTML).toMatchInlineSnapshot(`"
"`); - set(2); - expect(container.innerHTML).toMatchInlineSnapshot(`"

hello world 2

"`); - }); - - it('should not transform "and expression" whose right is identifier', ({ container }) => { - let set: (num: number) => void; - - function MyComp() { - let count = 0; - set = val => { - count = val; - }; - return
{count > 0 && count > 1 ?

hello world {count}

: `Empty`}
; - } - - render(MyComp, container); - set(0); - expect(container.innerHTML).toMatchInlineSnapshot(`"
Empty
"`); - set(1); - expect(container.innerHTML).toMatchInlineSnapshot(`"
Empty
"`); - set(2); - expect(container.innerHTML).toMatchInlineSnapshot(`"

hello world 2

"`); - }); - - it('should transform "condition expression in condition expression" to "if else tag in if else tag"', ({ - container, - }) => { - let set: (num: number) => void; - - function MyComp() { - let count = 0; - set = val => { - count = val; - }; - return
{count > 0 ? count > 1 ?

hello world {count}

: 'Empty2' : 'Empty1'}
; - } - - render(MyComp, container); - set(0); - expect(container.innerHTML).toMatchInlineSnapshot(`"
Empty1
"`); - set(1); - expect(container.innerHTML).toMatchInlineSnapshot(`"
Empty2
"`); - set(2); - expect(container.innerHTML).toMatchInlineSnapshot(`"

hello world 2

"`); - }); -}); - -describe('additional conditional rendering tests', () => { - it('Should correctly render content based on a single if condition', ({ container }) => { - let set: (num: number) => void; - - function App() { - let count = 0; - set = (val: number) => { - count = val; - }; - return ( -
- 5}>Count is greater than 5 -
- ); - } - - render(App, container); - expect(container.innerHTML).toBe('
'); - set(6); - expect(container.innerHTML).toBe('
Count is greater than 5
'); - }); - - it('Should correctly render content based on if-else conditions', ({ container }) => { - let set: (num: number) => void; - - function App() { - let count = 0; - set = (val: number) => { - count = val; - }; - return ( -
- Count is even - Count is odd -
- ); - } - - render(App, container); - expect(container.innerHTML).toBe('
Count is even
'); - set(1); - expect(container.innerHTML).toBe('
Count is odd
'); - set(2); - expect(container.innerHTML).toBe('
Count is even
'); - }); - - it('Should correctly render content based on if-else-if-else conditions', ({ container }) => { - let set: (num: number) => void; - - function App() { - let count = 0; - set = (val: number) => { - count = val; - }; - return ( -
- 10}>Count is greater than 10 - 5}>Count is greater than 5 but not greater than 10 - 0}>Count is greater than 0 but not greater than 5 - Count is 0 or negative -
- ); - } - - render(App, container); - expect(container.innerHTML).toBe('
Count is 0 or negative
'); - set(3); - expect(container.innerHTML).toBe('
Count is greater than 0 but not greater than 5
'); - set(7); - expect(container.innerHTML).toBe('
Count is greater than 5 but not greater than 10
'); - set(11); - expect(container.innerHTML).toBe('
Count is greater than 10
'); - }); - - it('Should correctly handle nested conditional rendering', ({ container }) => { - let set: (obj: { x: number; y: number }) => void; - - function App() { - let state = { x: 0, y: 0 }; - set = (val: { x: number; y: number }) => { - state = val; - }; - return ( -
- 0}> - X is positive - 0}> -
Both X and Y are positive
-
- -
X is positive but Y is not
-
-
- - X is not positive - 0}> -
X is not positive but Y is
-
- -
Neither X nor Y are positive
-
-
-
- ); - } - - render(App, container); - expect(container.innerHTML).toBe('
X is not positive
Neither X nor Y are positive
'); - set({ x: 1, y: 0 }); - expect(container.innerHTML).toBe('
X is positive
X is positive but Y is not
'); - set({ x: 1, y: 1 }); - expect(container.innerHTML).toBe('
X is positive
Both X and Y are positive
'); - set({ x: 0, y: 1 }); - expect(container.innerHTML).toBe('
X is not positive
X is not positive but Y is
'); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render } from '../src'; + +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); + +describe('conditional rendering', () => { + it('should if, else, else if', ({ container }) => { + let set: (num: number) => void; + + function App() { + let count = 2; + set = (val: number) => { + count = val; + }; + return ( + <> + 1}>{count} is bigger than is 1 + {count} is equal to 1 + {count} is smaller than 1 + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('2 is bigger than is 1'); + set(1); + expect(container.innerHTML).toBe('1 is equal to 1'); + set(0); + expect(container.innerHTML).toBe('0 is smaller than 1'); + }); + + it('should support nested if', ({ container }) => { + let set: (num: number) => void; + + function App() { + let count = 0; + set = (val: number) => { + count = val; + }; + return ( + 1}> + {count} is bigger than is 1 + 2}> +
{count} is bigger than is 2
+
+
+ ); + } + + render(App, container); + expect(container.innerHTML).toMatchInlineSnapshot(`""`); + set(2); + expect(container.innerHTML).toMatchInlineSnapshot(` + "2 is bigger than is 1" + `); + set(3); + expect(container.innerHTML).toMatchInlineSnapshot(` + "3 is bigger than is 1
3 is bigger than is 2
" + `); + set(2); + expect(container.innerHTML).toMatchInlineSnapshot(` + "2 is bigger than is 1" + `); + }); + it('should transform "and expression in and expression" to "if tag in if tag"', ({ container }) => { + let set: (num: number) => void; + + function MyComp() { + let count = 0; + set = (val: number) => { + count = val; + }; + return
{count > 0 && count > 1 &&

hello world {count}

}
; + } + + render(MyComp, container); + set(0); + expect(container.innerHTML).toMatchInlineSnapshot(`"
"`); + set(1); + expect(container.innerHTML).toMatchInlineSnapshot(`"
"`); + set(2); + expect(container.innerHTML).toMatchInlineSnapshot(`"

hello world 2

"`); + }); + + it('should not transform "and expression" whose right is identifier', ({ container }) => { + let set: (num: number) => void; + + function MyComp() { + let count = 0; + set = val => { + count = val; + }; + return
{count > 0 && count > 1 ?

hello world {count}

: `Empty`}
; + } + + render(MyComp, container); + set(0); + expect(container.innerHTML).toMatchInlineSnapshot(`"
Empty
"`); + set(1); + expect(container.innerHTML).toMatchInlineSnapshot(`"
Empty
"`); + set(2); + expect(container.innerHTML).toMatchInlineSnapshot(`"

hello world 2

"`); + }); + + it('should transform "condition expression in condition expression" to "if else tag in if else tag"', ({ + container, + }) => { + let set: (num: number) => void; + + function MyComp() { + let count = 0; + set = val => { + count = val; + }; + return
{count > 0 ? count > 1 ?

hello world {count}

: 'Empty2' : 'Empty1'}
; + } + + render(MyComp, container); + set(0); + expect(container.innerHTML).toMatchInlineSnapshot(`"
Empty1
"`); + set(1); + expect(container.innerHTML).toMatchInlineSnapshot(`"
Empty2
"`); + set(2); + expect(container.innerHTML).toMatchInlineSnapshot(`"

hello world 2

"`); + }); +}); + +describe('additional conditional rendering tests', () => { + it('Should correctly render content based on a single if condition', ({ container }) => { + let set: (num: number) => void; + + function App() { + let count = 0; + set = (val: number) => { + count = val; + }; + return ( +
+ 5}>Count is greater than 5 +
+ ); + } + + render(App, container); + expect(container.innerHTML).toBe('
'); + set(6); + expect(container.innerHTML).toBe('
Count is greater than 5
'); + }); + + it('Should correctly render content based on if-else conditions', ({ container }) => { + let set: (num: number) => void; + + function App() { + let count = 0; + set = (val: number) => { + count = val; + }; + return ( +
+ Count is even + Count is odd +
+ ); + } + + render(App, container); + expect(container.innerHTML).toBe('
Count is even
'); + set(1); + expect(container.innerHTML).toBe('
Count is odd
'); + set(2); + expect(container.innerHTML).toBe('
Count is even
'); + }); + + it('Should correctly render content based on if-else-if-else conditions', ({ container }) => { + let set: (num: number) => void; + + function App() { + let count = 0; + set = (val: number) => { + count = val; + }; + return ( +
+ 10}>Count is greater than 10 + 5}>Count is greater than 5 but not greater than 10 + 0}>Count is greater than 0 but not greater than 5 + Count is 0 or negative +
+ ); + } + + render(App, container); + expect(container.innerHTML).toBe('
Count is 0 or negative
'); + set(3); + expect(container.innerHTML).toBe('
Count is greater than 0 but not greater than 5
'); + set(7); + expect(container.innerHTML).toBe('
Count is greater than 5 but not greater than 10
'); + set(11); + expect(container.innerHTML).toBe('
Count is greater than 10
'); + }); + + it('Should correctly handle nested conditional rendering', ({ container }) => { + let set: (obj: { x: number; y: number }) => void; + + function App() { + let state = { x: 0, y: 0 }; + set = (val: { x: number; y: number }) => { + state = val; + }; + return ( +
+ 0}> + X is positive + 0}> +
Both X and Y are positive
+
+ +
X is positive but Y is not
+
+
+ + X is not positive + 0}> +
X is not positive but Y is
+
+ +
Neither X nor Y are positive
+
+
+
+ ); + } + + render(App, container); + expect(container.innerHTML).toBe('
X is not positive
Neither X nor Y are positive
'); + set({ x: 1, y: 0 }); + expect(container.innerHTML).toBe('
X is positive
X is positive but Y is not
'); + set({ x: 1, y: 1 }); + expect(container.innerHTML).toBe('
X is positive
Both X and Y are positive
'); + set({ x: 0, y: 1 }); + expect(container.innerHTML).toBe('
X is not positive
X is not positive but Y is
'); + }); +}); diff --git a/packages/inula-next/test/context.test.tsx b/packages/inula-next/test/context.test.tsx index 4541bbb81e4dd8a50d19b6bbee81e9fa9d44f485..62220d1b6ea4a92ca3058366f75a6ed127ab017c 100644 --- a/packages/inula-next/test/context.test.tsx +++ b/packages/inula-next/test/context.test.tsx @@ -1,348 +1,349 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render, createContext, useContext } from '../src'; - -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); - -describe('context', () => { - it('should support context', ({ container }) => { - const TheContext = createContext({ - theme: '', - }); - - function App() { - return ( - - - - ); - } - - function Child({ name }) { - const { theme } = useContext(TheContext); - return ( - <> - name is {name}, theme is {theme} - - ); - } - - render(App, container); - expect(container.innerHTML.trim()).toBe('name is child, theme is dark'); - }); - - it('should support recursive context', ({ container }) => { - const FileContext = createContext({ - level: 0, - }); - // Folder is the consumer and provider at same time - const Folder = ({ name, children }) => { - const { level } = useContext(FileContext); - return ( - -
-

{`Folder: ${name}, level: ${level}`}

- {children} -
-
- ); - }; - const File = ({ name }) => { - const { level } = useContext(FileContext); - - return
{`File: ${name}, level: ${level}`}
; - }; - - const App = () => { - return ( - - - - - - - ); - }; - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
-
-

- Folder: Root, level: 0 -

-
- File: file1.txt, level: 1 -
-
-

- Folder: Subfolder 2, level: 1 -

-
- File: file2.txt, level: 2 -
-
-
-
- `); - }); - - it('should support use context in conditional node', ({ container }) => { - const ThemeContext = createContext('light'); - - function Child({ name }) { - const { theme } = useContext(ThemeContext); - return ( - <> -

{name}

-
{theme}
- - ); - } - - let showChild; - - function App() { - let show = false; - showChild = () => { - show = true; - }; - return ( - <> - - - - - - - - - - - - - ); - } - - render(App, container); - expect(container.innerHTML).toMatchInlineSnapshot( - `"

False branch

dark

Side branch

light
"` - ); - showChild(); - expect(container.innerHTML).toMatchInlineSnapshot( - `"

True branch

dark

Side branch

light
"` - ); - }); - - it('should support use context in loop node', ({ container }) => { - const ItemContext = createContext(null); - const ThemeContext = createContext('light'); - let addItem; - - function ItemList() { - const items = ['Apple', 'Banana']; - addItem = item => { - items.push(item); - }; - return ( - -
    - - {item => ( - - - - )} - -
-
- ); - } - - function ListItem() { - const { item } = useContext(ItemContext); - const { theme } = useContext(ThemeContext); - return
  • {`${theme} - ${item}`}
  • ; - } - - render(ItemList, container); - expect(container.innerHTML).toMatchInlineSnapshot(`"
    • dark - Apple
    • dark - Banana
    "`); - addItem('grape'); - expect(container.innerHTML).toMatchInlineSnapshot( - `"
    • dark - Apple
    • dark - Banana
    • dark - grape
    "` - ); - }); - - it('should update context value', ({ container }) => { - const CountContext = createContext(0); - - function Counter() { - const { count } = useContext(CountContext); - return
    Count: {count}
    ; - } - - function App({ initialCount = 0 }) { - let count = initialCount; - const onClick = () => (count = count + 1); - return ( - - - - - ); - } - - render(App, container); - expect(container.querySelector('div').textContent).toBe('Count: 0'); - - container.querySelector('button').click(); - expect(container.querySelector('div').textContent).toBe('Count: 1'); - }); - it('Should correctly create and provide context', ({ container }) => { - const ThemeContext = createContext('light'); - - function App() { - return ( - - - - ); - } - - function Child() { - const { theme } = useContext(ThemeContext); - return
    Current theme: {theme}
    ; - } - - render(App, container); - expect(container.innerHTML).toBe('
    Current theme: dark
    '); - }); - - it('Should correctly consume context in child components', ({ container }) => { - const UserContext = createContext({ name: '', age: 0 }); - - function App() { - return ( - - - - ); - } - - function Parent() { - return ( -
    - - -
    - ); - } - - function Child1() { - const { name } = useContext(UserContext); - return
    Name: {name}
    ; - } - - function Child2() { - const { age } = useContext(UserContext); - return
    Age: {age}
    ; - } - - render(App, container); - expect(container.innerHTML).toBe('
    Name: Alice
    Age: 30
    '); - }); - - it('Should correctly update context and re-render consumers', ({ container }) => { - const CountContext = createContext(0); - - function App() { - let count = 0; - const increment = () => { - count += 1; - }; - - return ( - - - - - ); - } - - function Counter() { - const { count } = useContext(CountContext); - return
    Count: {count}
    ; - } - - render(App, container); - expect(container.querySelector('div').textContent).toBe('Count: 0'); - - container.querySelector('button').click(); - expect(container.querySelector('div').textContent).toBe('Count: 1'); - }); - - it('Should handle nested contexts correctly', ({ container }) => { - const ThemeContext = createContext('light'); - const LanguageContext = createContext('en'); - - function App() { - return ( - - - - - - ); - } - - function Child() { - const { theme } = useContext(ThemeContext); - const { language } = useContext(LanguageContext); - return ( -
    - Theme: {theme}, Language: {language} -
    - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    Theme: dark, Language: fr
    '); - }); - - it('Should use default value when no provider is present', ({ container }) => { - const DefaultContext = createContext({ message: 'Default message' }); - - function App() { - return ; - } - - function Child() { - const { message } = useContext(DefaultContext); - return
    {message}
    ; - } - - render(App, container); - expect(container.innerHTML).toBe('
    Default message
    '); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render } from '../src'; +import { createContext, useContext } from '../src/ContextNode'; + +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); + +describe('context', () => { + it('should support context', ({ container }) => { + const TheContext = createContext({ + theme: '', + }); + + function App() { + return ( + + + + ); + } + + function Child({ name }) { + const { theme } = useContext(TheContext); + return ( + <> + name is {name}, theme is {theme} + + ); + } + + render(App, container); + expect(container.innerHTML.trim()).toBe('name is child, theme is dark'); + }); + + it('should support recursive context', ({ container }) => { + const FileContext = createContext({ + level: 0, + }); + // Folder is the consumer and provider at same time + const Folder = ({ name, children }) => { + const { level } = useContext(FileContext); + return ( + +
    +

    {`Folder: ${name}, level: ${level}`}

    + {children} +
    +
    + ); + }; + const File = ({ name }) => { + const { level } = useContext(FileContext); + + return
    {`File: ${name}, level: ${level}`}
    ; + }; + + const App = () => { + return ( + + + + + + + ); + }; + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
    +

    + Folder: Root, level: 0 +

    +
    + File: file1.txt, level: 1 +
    +
    +

    + Folder: Subfolder 2, level: 1 +

    +
    + File: file2.txt, level: 2 +
    +
    +
    +
    + `); + }); + + it('should support use context in conditional node', ({ container }) => { + const ThemeContext = createContext('light'); + + function Child({ name }) { + const { theme } = useContext(ThemeContext); + return ( + <> +

    {name}

    +
    {theme}
    + + ); + } + + let showChild; + + function App() { + let show = false; + showChild = () => { + show = true; + }; + return ( + <> + + + + + + + + + + + + + ); + } + + render(App, container); + expect(container.innerHTML).toMatchInlineSnapshot( + `"

    False branch

    dark

    Side branch

    light
    "` + ); + showChild(); + expect(container.innerHTML).toMatchInlineSnapshot( + `"

    True branch

    dark

    Side branch

    light
    "` + ); + }); + + it('should support use context in loop node', ({ container }) => { + const ItemContext = createContext(null); + const ThemeContext = createContext('light'); + let addItem; + + function ItemList() { + const items = ['Apple', 'Banana']; + addItem = item => { + items.push(item); + }; + return ( + +
      + + {item => ( + + + + )} + +
    +
    + ); + } + + function ListItem() { + const { item } = useContext(ItemContext); + const { theme } = useContext(ThemeContext); + return
  • {`${theme} - ${item}`}
  • ; + } + + render(ItemList, container); + expect(container.innerHTML).toMatchInlineSnapshot(`"
    • dark - Apple
    • dark - Banana
    "`); + addItem('grape'); + expect(container.innerHTML).toMatchInlineSnapshot( + `"
    • dark - Apple
    • dark - Banana
    • dark - grape
    "` + ); + }); + + it('should update context value', ({ container }) => { + const CountContext = createContext(0); + + function Counter() { + const { count } = useContext(CountContext); + return
    Count: {count}
    ; + } + + function App({ initialCount = 0 }) { + let count = initialCount; + const onClick = () => (count = count + 1); + return ( + + + + + ); + } + + render(App, container); + expect(container.querySelector('div').textContent).toBe('Count: 0'); + + container.querySelector('button').click(); + expect(container.querySelector('div').textContent).toBe('Count: 1'); + }); + it('Should correctly create and provide context', ({ container }) => { + const ThemeContext = createContext('light'); + + function App() { + return ( + + + + ); + } + + function Child() { + const { theme } = useContext(ThemeContext); + return
    Current theme: {theme}
    ; + } + + render(App, container); + expect(container.innerHTML).toBe('
    Current theme: dark
    '); + }); + + it('Should correctly consume context in child components', ({ container }) => { + const UserContext = createContext({ name: '', age: 0 }); + + function App() { + return ( + + + + ); + } + + function Parent() { + return ( +
    + + +
    + ); + } + + function Child1() { + const { name } = useContext(UserContext); + return
    Name: {name}
    ; + } + + function Child2() { + const { age } = useContext(UserContext); + return
    Age: {age}
    ; + } + + render(App, container); + expect(container.innerHTML).toBe('
    Name: Alice
    Age: 30
    '); + }); + + it('Should correctly update context and re-render consumers', ({ container }) => { + const CountContext = createContext(0); + + function App() { + let count = 0; + const increment = () => { + count += 1; + }; + + return ( + + + + + ); + } + + function Counter() { + const { count } = useContext(CountContext); + return
    Count: {count}
    ; + } + + render(App, container); + expect(container.querySelector('div').textContent).toBe('Count: 0'); + + container.querySelector('button').click(); + expect(container.querySelector('div').textContent).toBe('Count: 1'); + }); + + it('Should handle nested contexts correctly', ({ container }) => { + const ThemeContext = createContext('light'); + const LanguageContext = createContext('en'); + + function App() { + return ( + + + + + + ); + } + + function Child() { + const { theme } = useContext(ThemeContext); + const { language } = useContext(LanguageContext); + return ( +
    + Theme: {theme}, Language: {language} +
    + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    Theme: dark, Language: fr
    '); + }); + + it('Should use default value when no provider is present', ({ container }) => { + const DefaultContext = createContext({ message: 'Default message' }); + + function App() { + return ; + } + + function Child() { + const { message } = useContext(DefaultContext); + return
    {message}
    ; + } + + render(App, container); + expect(container.innerHTML).toBe('
    Default message
    '); + }); +}); diff --git a/packages/inula-next/test/customHook.test.tsx b/packages/inula-next/test/customHook.test.tsx index b8a2e9708c3cbeeb228459d1a4c88e52ddf91f4f..afe113894c5d08b61a046f5ccbddf90bd281dd00 100644 --- a/packages/inula-next/test/customHook.test.tsx +++ b/packages/inula-next/test/customHook.test.tsx @@ -1,724 +1,738 @@ -/* - * 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, vi, beforeEach, afterEach } from 'vitest'; -import { domTest as it } from './utils'; - -import { didMount, didUnmount, render, useContext, createContext } from '../src'; - -// 模拟调度器 -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); - -// 自定义 Hook -function useCounter(initialValue = 0) { - let count = initialValue; - const setCount = () => { - count += 1; - }; - return { count, setCount }; -} - -describe('Custom Hook Tests', () => { - // 1. Basic Functionality - describe('Basic Functionality', () => { - it('should initialize with the correct value', ({ container }) => { - function TestComponent() { - const { count } = useCounter(5); - return
    {count}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    5
    '); - }); - - it('should update state correctly', ({ container }) => { - let setCountFromHook: () => void; - - function TestComponent() { - const { count, setCount } = useCounter(); - setCountFromHook = setCount; - return
    {count}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    0
    '); - setCountFromHook(); - expect(container.innerHTML).toBe('
    1
    '); - }); - - // Version accepting an object parameter - function useCounterWithObject({ initial = 0, step = 1 }) { - let count = initial; - const setCount = () => { - count += step; - }; - return { count, setCount }; - } - - // Version accepting multiple parameters - function useCounterWithMultipleParams(initial = 0, step = 1, max = Infinity) { - let count = initial; - const setCount = () => { - count = Math.min(count + step, max); - }; - return { count, setCount }; - } - - // Version returning an array - function useCounterReturningArray(initial = 0) { - let count = initial; - const setCount = () => { - count += 1; - }; - return [count, setCount]; - } - - // Test object input - it('should initialize with an object input', ({ container }) => { - function TestComponent() { - const { count } = useCounterWithObject({ initial: 10, step: 2 }); - return
    {count}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    10
    '); - }); - - // Test multiple variable inputs - it('should initialize with multiple variable inputs', ({ container }) => { - function TestComponent() { - const { count } = useCounterWithMultipleParams(15, 3, 20); - return
    {count}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    15
    '); - }); - - // Test single variable output - it('should return a single variable output', ({ container }) => { - function TestComponent() { - const { count } = useCounter(); - return
    {count}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    0
    '); - }); - - // Test object output - it('should return an object output', ({ container }) => { - let setCountFromHook: () => void; - - function TestComponent() { - const { count, setCount } = useCounter(); - setCountFromHook = setCount; - return
    {count}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    0
    '); - setCountFromHook(); - expect(container.innerHTML).toBe('
    1
    '); - }); - - // Test object input and update - it('should initialize with an object input and update correctly', ({ container }) => { - let incrementFromHook; - - function TestComponent() { - const { count, setCount } = useCounterWithObject({ initial: 10, step: 2 }); - incrementFromHook = setCount; - return
    {count}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    10
    '); - incrementFromHook(); - expect(container.innerHTML).toBe('
    12
    '); - }); - - // Test array output - it('should return an array output', ({ container }) => { - let setCountFromHook: () => void; - - function TestComponent() { - const [count, setCount] = useCounterReturningArray(); - setCountFromHook = setCount; - return
    {count}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    0
    '); - setCountFromHook(); - expect(container.innerHTML).toBe('
    1
    '); - }); - - // Test array output and update - it('should return an array output and update correctly', ({ container }) => { - let incrementFromHook; - - function TestComponent() { - const [count, increment] = useCounterReturningArray(5); - incrementFromHook = increment; - return
    {count}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    5
    '); - incrementFromHook(); - expect(container.innerHTML).toBe('
    6
    '); - }); - - // Test complex variable updates - it('should update state correctly with complex inputs', ({ container }) => { - let setCountFromHook: () => void; - - function TestComponent() { - const { count, setCount } = useCounterWithMultipleParams(5, 2, 10); - setCountFromHook = setCount; - return
    {count}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    5
    '); - setCountFromHook(); - expect(container.innerHTML).toBe('
    7
    '); - setCountFromHook(); - expect(container.innerHTML).toBe('
    9
    '); - setCountFromHook(); - expect(container.innerHTML).toBe('
    10
    '); // Max value reached - }); - }); - - // 2. Multiple Instances - describe('Multiple Instances', () => { - it('should maintain separate state for multiple instances', ({ container }) => { - let setCount1: () => void, setCount2: () => void; - - function TestComponent() { - const { count: count1, setCount: setCount1Hook } = useCounter(0); - const { count: count2, setCount: setCount2Hook } = useCounter(10); - setCount1 = setCount1Hook; - setCount2 = setCount2Hook; - return ( -
    - {count1}-{count2} -
    - ); - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    0-10
    '); - setCount1(); - setCount2(); - expect(container.innerHTML).toBe('
    1-11
    '); - }); - }); - - // 3. Lifecycle and Effects - describe('Lifecycle and Effects', () => { - it.fails('should run effects on mount and cleanup on unmount', ({ container }) => { - const mockEffect = vi.fn(); - const mockCleanup = vi.fn(); - - function useEffectTest() { - didMount(() => { - mockEffect(); - }); - didUnmount(() => { - mockCleanup(); - }); - } - - function TestComponent() { - useEffectTest(); - return null; - } - - render(TestComponent, container); - - expect(mockEffect).toHaveBeenCalledTimes(1); - // unmount(); - // expect(mockCleanup).toHaveBeenCalledTimes(1); - }); - }); - - // 4. Context Integration - describe('Context Integration', () => { - it('should consume context correctly', ({ container }) => { - const CountContext = createContext(0); - - function useContextCounter() { - const count = useContext(CountContext); - return count; - } - - function TestComponent() { - const { count } = useContextCounter(); - return
    {count}
    ; - } - - function App() { - return ( - - - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    5
    '); - }); - }); - - // 5. Props Handling - describe('Props Handling', () => { - it('should update when props change', ({ container }) => { - function usePropsTest({ initial }: { initial: number }) { - const value = initial; - return value * 2; - } - - let update: (n: number) => void; - - function TestComponent() { - let value = 5; - const hookValue = usePropsTest({ initial: value }); - update = (n: number) => (value = n); - return
    {hookValue}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    10
    '); - update(10); - expect(container.innerHTML).toBe('
    20
    '); - }); - }); - - // 6. Hook Nesting - describe('Hook Nesting', () => { - it('should handle nested hook calls correctly', ({ container }) => { - function useNestedCounter(initial: number) { - const { count, setCount } = useCounter(initial); - const doubleCount = count * 2; - return { count, doubleCount, setCount }; - } - - let setCountFromHook: () => void; - - function TestComponent() { - const { count, doubleCount, setCount } = useNestedCounter(5); - setCountFromHook = setCount; - return ( -
    - {count}-{doubleCount} -
    - ); - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    5-10
    '); - // @ts-ignore - setCountFromHook(); - expect(container.innerHTML).toBe('
    6-12
    '); - }); - }); - - // 7. Hook Computation - describe('hook return value computation', () => { - it('should receive props and output value', ({ container }) => { - let updateValue: (n: number) => void; - - function usePropsTest({ initial }: { initial: number }) { - return initial * 2; - } - - function TestComponent() { - let value = 1; - const hookValue = usePropsTest({ initial: value }); - let computed = hookValue * 2; - updateValue = n => (value = n); - return
    {computed}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    4
    '); - updateValue(2); - expect(container.innerHTML).toBe('
    8
    '); - }); - }); -}); - -describe('Hook and Watch Combined Tests', () => { - it('should update watched value when hook state changes', ({ container }) => { - let setCount; - function useCounter(initial = 0) { - let count = initial; - setCount = n => { - count = n; - }; - return { count }; - } - - function TestComponent() { - const { count } = useCounter(0); - let watchedCount = 0; - - watch(() => { - watchedCount = count * 2; - }); - - return
    {watchedCount}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    0
    '); - setCount(5); - expect(container.innerHTML).toBe('
    10
    '); - }); - - it('should handle multiple watches in a custom hook', ({ container }) => { - let setX, setY; - function usePosition() { - let x = 0, - y = 0; - setX = newX => { - x = newX; - }; - setY = newY => { - y = newY; - }; - - let position = ''; - watch(() => { - position = `(${x},${y})`; - }); - - let quadrant = 0; - watch(() => { - quadrant = x >= 0 && y >= 0 ? 1 : x < 0 && y >= 0 ? 2 : x < 0 && y < 0 ? 3 : 4; - }); - - return { position, quadrant }; - } - - function TestComponent() { - const { position, quadrant } = usePosition(); - return ( -
    - {position} Q{quadrant} -
    - ); - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    (0,0) Q1
    '); - setX(-5); - setY(10); - expect(container.innerHTML).toBe('
    (-5,10) Q2
    '); - }); - - it('should correctly handle watch dependencies in hooks', ({ container }) => { - let setItems; - function useFilteredList(initialItems = []) { - let items = initialItems; - setItems = newItems => { - items = newItems; - }; - - let evenItems = []; - let oddItems = []; - - watch(() => { - evenItems = items.filter(item => item % 2 === 0); - }); - - watch(() => { - oddItems = items.filter(item => item % 2 !== 0); - }); - - return { evenItems, oddItems }; - } - - function TestComponent() { - const { evenItems, oddItems } = useFilteredList([1, 2, 3, 4, 5]); - return ( -
    - Even: {evenItems.join(',')} Odd: {oddItems.join(',')} -
    - ); - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    Even: 2,4 Odd: 1,3,5
    '); - setItems([2, 4, 6, 8, 10]); - expect(container.innerHTML).toBe('
    Even: 2,4,6,8,10 Odd:
    '); - }); -}); - -describe('Advanced Hook Tests', () => { - // Hook return tests - describe('Hook Return Tests', () => { - it('should handle expression return', ({ container }) => { - function useExpression(a: number, b: number) { - return a + b * 2; - } - - function TestComponent() { - const result = useExpression(3, 4); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    11
    '); - }); - - it('should handle object spread return', ({ container }) => { - function useObjectSpread(obj: object) { - return { ...obj, newProp: 'added' }; - } - - function TestComponent() { - const result = useObjectSpread({ existingProp: 'original' }); - return
    {JSON.stringify(result)}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    {"existingProp":"original","newProp":"added"}
    '); - }); - - it('should handle function call return', ({ container }) => { - function useFunction() { - const innerFunction = () => 42; - return innerFunction(); - } - - function TestComponent() { - const result = useFunction(); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    42
    '); - }); - - it('should handle conditional expression return', ({ container }) => { - function useConditional(condition: boolean) { - return condition ? 'True' : 'False'; - } - - function TestComponent() { - const result = useConditional(true); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    True
    '); - }); - - it('should handle array computation return', ({ container }) => { - function useArrayComputation(arr: number[]) { - return arr.reduce((sum, num) => sum + num, 0); - } - - function TestComponent() { - const result = useArrayComputation([1, 2, 3, 4, 5]); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    15
    '); - }); - - it('should handle ternary expression return', ({ container }) => { - function useTernary(value: number) { - return value > 5 ? 'High' : value < 0 ? 'Low' : 'Medium'; - } - - function TestComponent() { - const result = useTernary(7); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    High
    '); - }); - - it('should handle member expression return', ({ container }) => { - function useMemberExpression(obj: { prop: string }) { - return obj.prop; - } - - function TestComponent() { - const result = useMemberExpression({ prop: 'test' }); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    test
    '); - }); - }); - - // Hook input tests - describe('Hook Input Tests', () => { - it('should handle expression input', ({ container }) => { - function useExpression(value: number) { - return value * 2; - } - - function TestComponent() { - const result = useExpression(3 + 4); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    14
    '); - }); - - it('should handle object spread input', ({ container }) => { - function useObjectSpread(obj: { a: number; b: number }) { - return obj.a + obj.b; - } - - function TestComponent() { - const baseObj = { a: 1, c: 3 }; - const result = useObjectSpread({ ...baseObj, b: 2 }); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    3
    '); - }); - - it('should handle function call input', ({ container }) => { - function useFunction(value: number) { - return value * 2; - } - - function getValue() { - return 21; - } - - function TestComponent() { - const result = useFunction(getValue()); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    42
    '); - }); - - it('should handle conditional expression input', ({ container }) => { - function useConditional(value: string) { - return `Received: ${value}`; - } - - function TestComponent() { - const condition = true; - const result = useConditional(condition ? 'Yes' : 'No'); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    Received: Yes
    '); - }); - - it('should handle array computation input', ({ container }) => { - function useArraySum(sum: number) { - return `Sum: ${sum}`; - } - - function TestComponent() { - const numbers = [1, 2, 3, 4, 5]; - const result = useArraySum(numbers.reduce((sum, num) => sum + num, 0)); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    Sum: 15
    '); - }); - - it('should handle ternary expression input', ({ container }) => { - function useStatus(status: string) { - return `Current status: ${status}`; - } - - function TestComponent() { - const value = 7; - const result = useStatus(value > 5 ? 'High' : value < 0 ? 'Low' : 'Medium'); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    Current status: High
    '); - }); - - it('should handle member expression input', ({ container }) => { - function useName(name: string) { - return `Hello, ${name}!`; - } - - function TestComponent() { - const user = { name: 'Alice' }; - const result = useName(user.name); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    Hello, Alice!
    '); - }); - }); - - // Additional tests - describe('Additional Hook Tests', () => { - it('should handle input based on other variables', ({ container }) => { - function useComputed(value: number) { - return value * 2; - } - - function TestComponent() { - let baseValue = 5; - let multiplier = 3; - const result = useComputed(baseValue * multiplier); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    30
    '); - }); - - it('should handle function input and output', ({ container }) => { - function useFunction(fn: (x: number) => number) { - return (y: number) => fn(y) * 2; - } - - function TestComponent() { - const inputFn = (x: number) => x + 1; - const resultFn = useFunction(inputFn); - const result = resultFn(5); - return
    {result}
    ; - } - - render(TestComponent, container); - expect(container.innerHTML).toBe('
    12
    '); - }); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; + +import { didMount, didUnMount, render } from '../src'; +import { useContext, createContext } from '../src/ContextNode'; + +// 模拟调度器 +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); + +// 自定义 Hook +function useCounter(initialValue = 0) { + let count = initialValue; + const setCount = () => { + count += 1; + }; + return { count, setCount }; +} + +describe('Custom Hook Tests', () => { + // 1. Basic Functionality + describe('Basic Functionality', () => { + it('should initialize with the correct value', ({ container }) => { + function TestComponent() { + const { count } = useCounter(5); + return
    {count}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    5
    '); + }); + + it('should update state correctly', ({ container }) => { + let setCountFromHook: () => void; + + function TestComponent() { + const { count, setCount } = useCounter(); + setCountFromHook = setCount; + return
    {count}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    0
    '); + setCountFromHook(); + expect(container.innerHTML).toBe('
    1
    '); + }); + + // Version accepting an object parameter + function useCounterWithObject({ initial = 0, step = 1 }) { + let count = initial; + const setCount = () => { + count += step; + }; + return { count, setCount }; + } + + // Version accepting multiple parameters + function useCounterWithMultipleParams(initial = 0, step = 1, max = Infinity) { + let count = initial; + const setCount = () => { + count = Math.min(count + step, max); + }; + return { count, setCount }; + } + + // Version returning an array + function useCounterReturningArray(initial = 0) { + let count = initial; + const setCount = () => { + count += 1; + }; + return [count, setCount]; + } + + // Test object input + it('should initialize with an object input', ({ container }) => { + function TestComponent() { + const { count } = useCounterWithObject({ initial: 10, step: 2 }); + return
    {count}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    10
    '); + }); + + // Test multiple variable inputs + it('should initialize with multiple variable inputs', ({ container }) => { + function TestComponent() { + const { count } = useCounterWithMultipleParams(15, 3, 20); + return
    {count}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    15
    '); + }); + + it('should support derived by state', ({ container }) => { + let updateCount: (max: number) => void; + function TestComponent() { + let init = 15; + updateCount = (value: number) => (init = value); + const { count } = useCounterWithMultipleParams(init, 3, 20); + return
    {count}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    15
    '); + updateCount(10); + expect(container.innerHTML).toBe('
    10
    '); + }); + + // Test single variable output + it('should return a single variable output', ({ container }) => { + function TestComponent() { + const { count } = useCounter(); + return
    {count}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    0
    '); + }); + + // Test object output + it('should return an object output', ({ container }) => { + let setCountFromHook: () => void; + + function TestComponent() { + const { count, setCount } = useCounter(); + setCountFromHook = setCount; + return
    {count}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    0
    '); + setCountFromHook(); + expect(container.innerHTML).toBe('
    1
    '); + }); + + // Test object input and update + it('should initialize with an object input and update correctly', ({ container }) => { + let incrementFromHook; + + function TestComponent() { + const { count, setCount } = useCounterWithObject({ initial: 10, step: 2 }); + incrementFromHook = setCount; + return
    {count}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    10
    '); + incrementFromHook(); + expect(container.innerHTML).toBe('
    12
    '); + }); + + // Test array output + it('should return an array output', ({ container }) => { + let setCountFromHook: () => void; + + function TestComponent() { + const [count, setCount] = useCounterReturningArray(); + setCountFromHook = setCount; + return
    {count}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    0
    '); + setCountFromHook(); + expect(container.innerHTML).toBe('
    1
    '); + }); + + // Test array output and update + it('should return an array output and update correctly', ({ container }) => { + let incrementFromHook; + + function TestComponent() { + const [count, increment] = useCounterReturningArray(5); + incrementFromHook = increment; + return
    {count}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    5
    '); + incrementFromHook(); + expect(container.innerHTML).toBe('
    6
    '); + }); + + // Test complex variable updates + it('should update state correctly with complex inputs', ({ container }) => { + let setCountFromHook: () => void; + + function TestComponent() { + const { count, setCount } = useCounterWithMultipleParams(5, 2, 10); + setCountFromHook = setCount; + return
    {count}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    5
    '); + setCountFromHook(); + expect(container.innerHTML).toBe('
    7
    '); + setCountFromHook(); + expect(container.innerHTML).toBe('
    9
    '); + setCountFromHook(); + expect(container.innerHTML).toBe('
    10
    '); // Max value reached + }); + }); + + // 2. Multiple Instances + describe('Multiple Instances', () => { + it('should maintain separate state for multiple instances', ({ container }) => { + let setCount1: () => void, setCount2: () => void; + + function TestComponent() { + const { count: count1, setCount: setCount1Hook } = useCounter(0); + const { count: count2, setCount: setCount2Hook } = useCounter(10); + setCount1 = setCount1Hook; + setCount2 = setCount2Hook; + return ( +
    + {count1}-{count2} +
    + ); + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    0-10
    '); + setCount1(); + setCount2(); + expect(container.innerHTML).toBe('
    1-11
    '); + }); + }); + + // 3. Lifecycle and Effects + describe('Lifecycle and Effects', () => { + it.fails('should run effects on mount and cleanup on unmount', ({ container }) => { + const mockEffect = vi.fn(); + const mockCleanup = vi.fn(); + + function useEffectTest() { + didMount(() => { + mockEffect(); + }); + didUnMount(() => { + mockCleanup(); + }); + } + + function TestComponent() { + useEffectTest(); + return null; + } + + render(TestComponent, container); + + expect(mockEffect).toHaveBeenCalledTimes(1); + }); + }); + + // 4. Context Integration + describe('Context Integration', () => { + it('should consume context correctly', ({ container }) => { + const CountContext = createContext(0); + + function useContextCounter() { + const count = useContext(CountContext); + return count; + } + + function TestComponent() { + const { count } = useContextCounter(); + return
    {count}
    ; + } + + function App() { + return ( + + + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    5
    '); + }); + }); + + // 5. Props Handling + describe('Props Handling', () => { + it('should update when props change', ({ container }) => { + function usePropsTest({ initial }: { initial: number }) { + const value = initial; + return value * 2; + } + + let update: (n: number) => void; + + function TestComponent() { + let value = 5; + const hookValue = usePropsTest({ initial: value }); + update = (n: number) => (value = n); + return
    {hookValue}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    10
    '); + update(10); + expect(container.innerHTML).toBe('
    20
    '); + }); + }); + + // 6. Hook Nesting + describe('Hook Nesting', () => { + it('should handle nested hook calls correctly', ({ container }) => { + function useNestedCounter(initial: number) { + const { count, setCount } = useCounter(initial); + const doubleCount = count * 2; + return { count, doubleCount, setCount }; + } + + let setCountFromHook: () => void; + + function TestComponent() { + const { count, doubleCount, setCount } = useNestedCounter(5); + setCountFromHook = setCount; + return ( +
    + {count}-{doubleCount} +
    + ); + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    5-10
    '); + // @ts-ignore + setCountFromHook(); + expect(container.innerHTML).toBe('
    6-12
    '); + }); + }); + + // 7. Hook Computation + describe('hook return value computation', () => { + it('should receive props and output value', ({ container }) => { + let updateValue: (n: number) => void; + + function usePropsTest({ initial }: { initial: number }) { + return initial * 2; + } + + function TestComponent() { + let value = 1; + const hookValue = usePropsTest({ initial: value }); + let computed = hookValue * 2; + updateValue = n => (value = n); + return
    {computed}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    4
    '); + updateValue(2); + expect(container.innerHTML).toBe('
    8
    '); + }); + }); +}); + +describe('Hook and Watch Combined Tests', () => { + it('should update watched value when hook state changes', ({ container }) => { + let setCount; + function useCounter(initial = 0) { + let count = initial; + setCount = n => { + count = n; + }; + return { count }; + } + + function TestComponent() { + const { count } = useCounter(0); + let watchedCount = 0; + + watch(() => { + watchedCount = count * 2; + }); + + return
    {watchedCount}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    0
    '); + setCount(5); + expect(container.innerHTML).toBe('
    10
    '); + }); + + it('should handle multiple watches in a custom hook', ({ container }) => { + let setX, setY; + function usePosition() { + let x = 0, + y = 0; + setX = newX => { + x = newX; + }; + setY = newY => { + y = newY; + }; + + let position = ''; + watch(() => { + position = `(${x},${y})`; + }); + + let quadrant = 0; + watch(() => { + quadrant = x >= 0 && y >= 0 ? 1 : x < 0 && y >= 0 ? 2 : x < 0 && y < 0 ? 3 : 4; + }); + + return { position, quadrant }; + } + + function TestComponent() { + const { position, quadrant } = usePosition(); + return ( +
    + {position} Q{quadrant} +
    + ); + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    (0,0) Q1
    '); + setX(-5); + setY(10); + expect(container.innerHTML).toBe('
    (-5,10) Q2
    '); + }); + + it('should correctly handle watch dependencies in hooks', ({ container }) => { + let setItems; + function useFilteredList(initialItems = []) { + let items = initialItems; + setItems = newItems => { + items = newItems; + }; + + let evenItems = []; + let oddItems = []; + + watch(() => { + evenItems = items.filter(item => item % 2 === 0); + }); + + watch(() => { + oddItems = items.filter(item => item % 2 !== 0); + }); + + return { evenItems, oddItems }; + } + + function TestComponent() { + const { evenItems, oddItems } = useFilteredList([1, 2, 3, 4, 5]); + return ( +
    + Even: {evenItems.join(',')} Odd: {oddItems.join(',')} +
    + ); + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    Even: 2,4 Odd: 1,3,5
    '); + setItems([2, 4, 6, 8, 10]); + expect(container.innerHTML).toBe('
    Even: 2,4,6,8,10 Odd:
    '); + }); +}); + +describe('Advanced Hook Tests', () => { + // Hook return tests + describe('Hook Return Tests', () => { + it('should handle expression return', ({ container }) => { + function useExpression(a: number, b: number) { + return a + b * 2; + } + + function TestComponent() { + const result = useExpression(3, 4); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    11
    '); + }); + + it('should handle object spread return', ({ container }) => { + function useObjectSpread(obj: object) { + return { ...obj, newProp: 'added' }; + } + + function TestComponent() { + const result = useObjectSpread({ existingProp: 'original' }); + return
    {JSON.stringify(result)}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    {"existingProp":"original","newProp":"added"}
    '); + }); + + it('should handle function call return', ({ container }) => { + function useFunction() { + const innerFunction = () => 42; + return innerFunction(); + } + + function TestComponent() { + const result = useFunction(); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    42
    '); + }); + + it('should handle conditional expression return', ({ container }) => { + function useConditional(condition: boolean) { + return condition ? 'True' : 'False'; + } + + function TestComponent() { + const result = useConditional(true); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    True
    '); + }); + + it('should handle array computation return', ({ container }) => { + function useArrayComputation(arr: number[]) { + return arr.reduce((sum, num) => sum + num, 0); + } + + function TestComponent() { + const result = useArrayComputation([1, 2, 3, 4, 5]); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    15
    '); + }); + + it('should handle ternary expression return', ({ container }) => { + function useTernary(value: number) { + return value > 5 ? 'High' : value < 0 ? 'Low' : 'Medium'; + } + + function TestComponent() { + const result = useTernary(7); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    High
    '); + }); + + it('should handle member expression return', ({ container }) => { + function useMemberExpression(obj: { prop: string }) { + return obj.prop; + } + + function TestComponent() { + const result = useMemberExpression({ prop: 'test' }); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    test
    '); + }); + }); + + // Hook input tests + describe('Hook Input Tests', () => { + it('should handle expression input', ({ container }) => { + function useExpression(value: number) { + return value * 2; + } + + function TestComponent() { + const result = useExpression(3 + 4); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    14
    '); + }); + + it('should handle object spread input', ({ container }) => { + function useObjectSpread(obj: { a: number; b: number }) { + return obj.a + obj.b; + } + + function TestComponent() { + const baseObj = { a: 1, c: 3 }; + const result = useObjectSpread({ ...baseObj, b: 2 }); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    3
    '); + }); + + it('should handle function call input', ({ container }) => { + function useFunction(value: number) { + return value * 2; + } + + function getValue() { + return 21; + } + + function TestComponent() { + const result = useFunction(getValue()); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    42
    '); + }); + + it('should handle conditional expression input', ({ container }) => { + function useConditional(value: string) { + return `Received: ${value}`; + } + + function TestComponent() { + const condition = true; + const result = useConditional(condition ? 'Yes' : 'No'); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    Received: Yes
    '); + }); + + it('should handle array computation input', ({ container }) => { + function useArraySum(sum: number) { + return `Sum: ${sum}`; + } + + function TestComponent() { + const numbers = [1, 2, 3, 4, 5]; + const result = useArraySum(numbers.reduce((sum, num) => sum + num, 0)); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    Sum: 15
    '); + }); + + it('should handle ternary expression input', ({ container }) => { + function useStatus(status: string) { + return `Current status: ${status}`; + } + + function TestComponent() { + const value = 7; + const result = useStatus(value > 5 ? 'High' : value < 0 ? 'Low' : 'Medium'); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    Current status: High
    '); + }); + + it('should handle member expression input', ({ container }) => { + function useName(name: string) { + return `Hello, ${name}!`; + } + + function TestComponent() { + const user = { name: 'Alice' }; + const result = useName(user.name); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    Hello, Alice!
    '); + }); + }); + + // Additional tests + describe('Additional Hook Tests', () => { + it('should handle input based on other variables', ({ container }) => { + function useComputed(value: number) { + return value * 2; + } + + function TestComponent() { + let baseValue = 5; + let multiplier = 3; + const result = useComputed(baseValue * multiplier); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    30
    '); + }); + + it('should handle function input and output', ({ container }) => { + function useFunction(fn: (x: number) => number) { + return (y: number) => fn(y) * 2; + } + + function TestComponent() { + const inputFn = (x: number) => x + 1; + const resultFn = useFunction(inputFn); + const result = resultFn(5); + return
    {result}
    ; + } + + render(TestComponent, container); + expect(container.innerHTML).toBe('
    12
    '); + }); + }); +}); diff --git a/packages/inula-next/test/event.test.tsx b/packages/inula-next/test/event.test.tsx index a0a49c0c51bbb281b6a33dff4a919636ed8d38c1..7d37e1471fc67743ec6b2e7ca4e0275422d8c7aa 100644 --- a/packages/inula-next/test/event.test.tsx +++ b/packages/inula-next/test/event.test.tsx @@ -1,556 +1,543 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render } from '../src'; - -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); - -describe('Event Handling', () => { - it('Should correctly handle onClick events', ({ container }) => { - let clicked = false; - function App() { - const handleClick = () => { - clicked = true; - }; - return ; - } - - render(App, container); - const button = container.querySelector('button'); - button.click(); - expect(clicked).toBe(true); - }); - - it('Should correctly handle onMouseOver events', ({ container }) => { - let hovered = false; - function App() { - const handleMouseOver = () => { - hovered = true; - }; - return ( -
    - Hover me -
    - ); - } - render(App, container); - const evObj = document.createEvent('Events'); - evObj.initEvent('mouseover', true, false); - document.getElementById('div_id').dispatchEvent(evObj); - expect(hovered).toBe(true); - }); - - it('Should correctly handle onKeyPress events', ({ container }) => { - let keypressed = ''; - function App() { - const handleKeyPress = event => { - keypressed = event.key; - }; - return ; - } - - render(App, container); - const input = container.querySelector('input'); - const event = new KeyboardEvent('keypress', { key: 'A' }); - input.dispatchEvent(event); - expect(keypressed).toBe('A'); - }); - - it('Should correctly handle onSubmit events', ({ container }) => { - let submitted = false; - function App() { - const handleSubmit = event => { - event.preventDefault(); - submitted = true; - }; - return ( -
    - -
    - ); - } - - render(App, container); - const form = container.querySelector('form'); - form.dispatchEvent(new Event('submit')); - expect(submitted).toBe(true); - }); - - it('Should correctly handle custom events', ({ container }) => { - let customEventData = null; - function App() { - const handleCustomEvent = event => { - customEventData = event.detail; - }; - return ( -
    - Custom event target -
    - ); - } - - render(App, container); - const evObj = document.createEvent('Events'); - evObj.detail = { message: 'Hello, Custom Event!' }; - evObj.initEvent('customevent', true, false); - document.getElementById('id').dispatchEvent(evObj); - expect(customEventData).toEqual({ message: 'Hello, Custom Event!' }); - }); - - it('Should correctly handle events when the handler is a variable', ({ container }) => { - let count = 0; - function App() { - const incrementCount = () => { - count++; - }; - return ; - } - - render(App, container); - const button = container.querySelector('button'); - button.click(); - expect(count).toBe(1); - }); - - it('Should correctly handle events when the handler is an expression returning a function', ({ container }) => { - let lastClicked = ''; - function App() { - const createHandler = buttonName => () => { - lastClicked = buttonName; - }; - return ( -
    - - -
    - ); - } - - render(App, container); - const buttons = container.querySelectorAll('button'); - buttons[0].click(); - expect(lastClicked).toBe('Button A'); - buttons[1].click(); - expect(lastClicked).toBe('Button B'); - }); -}); - -describe('event emission', () => { - it('should handle emit to parent', ({ container }) => { - function AnswerButton({ onYes, onNo }) { - return ( - <> - - - {/**/} - - ); - } - function App() { - let isHappy = false; - - function onAnswerNo() { - isHappy = false; - } - - function onAnswerYes() { - isHappy = true; - } - - return ( - <> -

    Are you happy?

    - -

    {isHappy ? 'yes' : 'no'}

    - - ); - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -

    - Are you happy? -

    - -

    - no -

    -
    - `); - container.querySelector('button')?.click(); - expect(container).toMatchInlineSnapshot(` -
    -

    - Are you happy? -

    - -

    - yes -

    -
    - `); - }); - it('should correctly emit events to parent component', ({ container }) => { - function Child({ onEvent }) { - return ; - } - - function App() { - let eventReceived = '1'; - - function handleEvent(event) { - eventReceived = event; - } - - return ( - <> - -

    {eventReceived}

    - - ); - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    - -

    - 1 -

    -
    - `); - - container.querySelector('button')?.click(); - expect(container).toMatchInlineSnapshot(` -
    - -

    - clicked -

    -
    - `); - }); - - it('should correctly update parent state based on emitted events', ({ container }) => { - function Counter({ onIncrement, onDecrement }) { - return ( -
    - - -
    - ); - } - - function App() { - let count = 0; - - function increment() { - count += 1; - } - - function decrement() { - count -= 1; - } - - return ( - <> - -

    Count:{count}

    - - ); - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
    - - -
    -

    - Count: - 0 -

    -
    - `); - - container.querySelectorAll('button')[0]?.click(); - expect(container).toMatchInlineSnapshot(` -
    -
    - - -
    -

    - Count: - 1 -

    -
    - `); - - container.querySelectorAll('button')[1]?.click(); - expect(container).toMatchInlineSnapshot(` -
    -
    - - -
    -

    - Count: - 0 -

    -
    - `); - }); - - it('should correctly handle multiple event emissions', ({ container }) => { - function MultiButton({ onClickA, onClickB, onClickC }) { - return ( -
    - - - -
    - ); - } - - function App() { - let lastClicked = 'A'; - - function handleClick(button) { - lastClicked = button; - } - - return ( - <> - handleClick('A')} - onClickB={() => handleClick('B')} - onClickC={() => handleClick('C')} - /> -

    Last clicked:{lastClicked}

    - - ); - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
    - - - -
    -

    - Last clicked: - A -

    -
    - `); - - container.querySelectorAll('button')[1]?.click(); - expect(container).toMatchInlineSnapshot(` -
    -
    - - - -
    -

    - Last clicked: - B -

    -
    - `); - }); - - it('should handle both arrow functions and function variables', ({ container }) => { - function Child({ onEventA, onEventB }) { - return ( -
    - - -
    - ); - } - - function App() { - let eventResult = '1'; - - const handleEventA = () => { - eventResult = 'Arrow function called'; - }; - - function handleEventB() { - eventResult = 'Function variable called'; - } - - return ( - <> - -

    {eventResult}

    - - ); - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
    - - -
    -

    - 1 -

    -
    - `); - - container.querySelectorAll('button')[0]?.click(); - expect(container).toMatchInlineSnapshot(` -
    -
    - - -
    -

    - Arrow function called -

    -
    - `); - - container.querySelectorAll('button')[1]?.click(); - expect(container).toMatchInlineSnapshot(` -
    -
    - - -
    -

    - Function variable called -

    -
    - `); - }); - - it('should handle multi-layer event functions', ({ container }) => { - function GrandChild({ onEvent }) { - return ; - } - - function Child({ onParentEvent }) { - function handleChildEvent(message) { - onParentEvent(`Child received: ${message}`); - } - - return ; - } - - function App() { - let message = '1'; - - function handleAppEvent(receivedMessage) { - message = `App received: ${receivedMessage}`; - } - - return ( - <> - -

    {message}

    - - ); - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    - -

    - 1 -

    -
    - `); - - container.querySelector('button')?.click(); - expect(container).toMatchInlineSnapshot(` -
    - -

    - App received: Child received: GrandChild clicked -

    -
    - `); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render } from '../src'; + +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); + +describe('Event Handling', () => { + it('Should correctly handle onClick events', ({ container }) => { + let clicked = false; + function App() { + const handleClick = () => { + clicked = true; + }; + return ; + } + + render(App, container); + const button = container.querySelector('button'); + button.click(); + expect(clicked).toBe(true); + }); + + it.fails('Should correctly handle onMouseOver events', ({ container }) => { + let hovered = false; + function App() { + const handleMouseOver = () => { + hovered = true; + }; + return
    Hover me
    ; + } + + render(App, container); + const div = container.querySelector('div'); + div.dispatchEvent(new MouseEvent('mouseover')); + expect(hovered).toBe(true); + }); + + it('Should correctly handle onKeyPress events', ({ container }) => { + let keypressed = ''; + function App() { + const handleKeyPress = event => { + keypressed = event.key; + }; + return ; + } + + render(App, container); + const input = container.querySelector('input'); + const event = new KeyboardEvent('keypress', { key: 'A' }); + input.dispatchEvent(event); + expect(keypressed).toBe('A'); + }); + + it('Should correctly handle onSubmit events', ({ container }) => { + let submitted = false; + function App() { + const handleSubmit = event => { + event.preventDefault(); + submitted = true; + }; + return ( +
    + +
    + ); + } + + render(App, container); + const form = container.querySelector('form'); + form.dispatchEvent(new Event('submit')); + expect(submitted).toBe(true); + }); + + it.fails('Should correctly handle custom events', ({ container }) => { + let customEventData = null; + function App() { + const handleCustomEvent = event => { + customEventData = event.detail; + }; + return
    Custom event target
    ; + } + + render(App, container); + const div = container.querySelector('div'); + const customEvent = new CustomEvent('customEvent', { detail: { message: 'Hello, Custom Event!' } }); + div.dispatchEvent(customEvent); + expect(customEventData).toEqual({ message: 'Hello, Custom Event!' }); + }); + + it('Should correctly handle events when the handler is a variable', ({ container }) => { + let count = 0; + function App() { + const incrementCount = () => { + count++; + }; + return ; + } + + render(App, container); + const button = container.querySelector('button'); + button.click(); + expect(count).toBe(1); + }); + + it('Should correctly handle events when the handler is an expression returning a function', ({ container }) => { + let lastClicked = ''; + function App() { + const createHandler = buttonName => () => { + lastClicked = buttonName; + }; + return ( +
    + + +
    + ); + } + + render(App, container); + const buttons = container.querySelectorAll('button'); + buttons[0].click(); + expect(lastClicked).toBe('Button A'); + buttons[1].click(); + expect(lastClicked).toBe('Button B'); + }); +}); + +describe('event emission', () => { + it('should handle emit to parent', ({ container }) => { + function AnswerButton({ onYes, onNo }) { + return ( + <> + + + {/**/} + + ); + } + function App() { + let isHappy = false; + + function onAnswerNo() { + isHappy = false; + } + + function onAnswerYes() { + isHappy = true; + } + + return ( + <> +

    Are you happy?

    + +

    {isHappy ? 'yes' : 'no'}

    + + ); + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +

    + Are you happy? +

    + +

    + no +

    +
    + `); + container.querySelector('button')?.click(); + expect(container).toMatchInlineSnapshot(` +
    +

    + Are you happy? +

    + +

    + yes +

    +
    + `); + }); + it('should correctly emit events to parent component', ({ container }) => { + function Child({ onEvent }) { + return ; + } + + function App() { + let eventReceived = '1'; + + function handleEvent(event) { + eventReceived = event; + } + + return ( + <> + +

    {eventReceived}

    + + ); + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    + +

    + 1 +

    +
    + `); + + container.querySelector('button')?.click(); + expect(container).toMatchInlineSnapshot(` +
    + +

    + clicked +

    +
    + `); + }); + + it('should correctly update parent state based on emitted events', ({ container }) => { + function Counter({ onIncrement, onDecrement }) { + return ( +
    + + +
    + ); + } + + function App() { + let count = 0; + + function increment() { + count += 1; + } + + function decrement() { + count -= 1; + } + + return ( + <> + +

    Count:{count}

    + + ); + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
    + + +
    +

    + Count: + 0 +

    +
    + `); + + container.querySelectorAll('button')[0]?.click(); + expect(container).toMatchInlineSnapshot(` +
    +
    + + +
    +

    + Count: + 1 +

    +
    + `); + + container.querySelectorAll('button')[1]?.click(); + expect(container).toMatchInlineSnapshot(` +
    +
    + + +
    +

    + Count: + 0 +

    +
    + `); + }); + + it('should correctly handle multiple event emissions', ({ container }) => { + function MultiButton({ onClickA, onClickB, onClickC }) { + return ( +
    + + + +
    + ); + } + + function App() { + let lastClicked = 'A'; + + function handleClick(button) { + lastClicked = button; + } + + return ( + <> + handleClick('A')} + onClickB={() => handleClick('B')} + onClickC={() => handleClick('C')} + /> +

    Last clicked:{lastClicked}

    + + ); + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
    + + + +
    +

    + Last clicked: + A +

    +
    + `); + + container.querySelectorAll('button')[1]?.click(); + expect(container).toMatchInlineSnapshot(` +
    +
    + + + +
    +

    + Last clicked: + B +

    +
    + `); + }); + + it('should handle both arrow functions and function variables', ({ container }) => { + function Child({ onEventA, onEventB }) { + return ( +
    + + +
    + ); + } + + function App() { + let eventResult = '1'; + + const handleEventA = () => { + eventResult = 'Arrow function called'; + }; + + function handleEventB() { + eventResult = 'Function variable called'; + } + + return ( + <> + +

    {eventResult}

    + + ); + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
    + + +
    +

    + 1 +

    +
    + `); + + container.querySelectorAll('button')[0]?.click(); + expect(container).toMatchInlineSnapshot(` +
    +
    + + +
    +

    + Arrow function called +

    +
    + `); + + container.querySelectorAll('button')[1]?.click(); + expect(container).toMatchInlineSnapshot(` +
    +
    + + +
    +

    + Function variable called +

    +
    + `); + }); + + it('should handle multi-layer event functions', ({ container }) => { + function GrandChild({ onEvent }) { + return ; + } + + function Child({ onParentEvent }) { + function handleChildEvent(message) { + onParentEvent(`Child received: ${message}`); + } + + return ; + } + + function App() { + let message = '1'; + + function handleAppEvent(receivedMessage) { + message = `App received: ${receivedMessage}`; + } + + return ( + <> + +

    {message}

    + + ); + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    + +

    + 1 +

    +
    + `); + + container.querySelector('button')?.click(); + expect(container).toMatchInlineSnapshot(` +
    + +

    + App received: Child received: GrandChild clicked +

    +
    + `); + }); +}); diff --git a/packages/inula-next/test/for.test.tsx b/packages/inula-next/test/for.test.tsx index 22d5258c4c3b7dd65d1562b3bd1024e49ea002d3..6420d3ea0e7faa9b84b6b25c4de9e69bfac569b6 100644 --- a/packages/inula-next/test/for.test.tsx +++ b/packages/inula-next/test/for.test.tsx @@ -1,225 +1,225 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render } from '../src'; - -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); -describe('for', () => { - it('should work', ({ container }) => { - function App() { - const arr = [0, 1, 2]; - return {item =>
    {item}
    }
    ; - } - - render(App, container); - expect(container.innerHTML).toMatchInlineSnapshot(`"
    0
    1
    2
    "`); - }); - - it('should update item when arr changed', ({ container }) => { - let updateArr: (num: number) => void; - - function App() { - const arr = [0, 1, 2]; - updateArr = (num: number) => { - arr.push(num); - }; - return {item =>
    {item}
    }
    ; - } - - render(App, container); - expect(container.children.length).toEqual(3); - updateArr(3); - expect(container.children.length).toEqual(4); - expect(container.innerHTML).toMatchInlineSnapshot(`"
    0
    1
    2
    3
    "`); - }); - - it('should get index', ({ container }) => { - let update: (num: number) => void; - function App() { - const arr = [0, 1, 2]; - update = (num: number) => { - arr.push(num); - }; - return {(item, index) =>
    {index}
    }
    ; - } - - render(App, container); - expect(container.innerHTML).toMatchInlineSnapshot(`"
    0
    1
    2
    "`); - update(3); - expect(container.innerHTML).toMatchInlineSnapshot(`"
    0
    1
    2
    3
    "`); - }); - it('should transform for loop', ({ container }) => { - function MyComp() { - let name = 'test'; - let arr = [ - { x: 1, y: 1 }, - { x: 2, y: 2 }, - { x: 3, y: 3 }, - ]; - return ( - - {({ x, y }, index) => { - let name1 = 'test'; - const onClick = () => { - name1 = 'changed'; - }; - return ( -
    - {name1} - {index} -
    - ); - }} -
    - ); - } - - render(MyComp, container); - expect(container.innerHTML).toBe( - '
    test0
    test1
    test2
    ' - ); - const item = container.querySelector('#item0') as HTMLDivElement; - item.click(); - expect(container.innerHTML).toBe( - '
    changed0
    test1
    test2
    ' - ); - }); - - it('should transform map to for jsx element', ({ container }) => { - function MyComp() { - const arr = [1, 2, 3]; - return ( - <> - {arr.map(item => ( -
    {item}
    - ))} - - ); - } - - render(MyComp, container); - expect(container.innerHTML).toBe('
    1
    2
    3
    '); - }); - it('should transform map in map to for', ({ container }) => { - function MyComp() { - const matrix = [ - [1, 2], - [3, 4], - ]; - return
    {matrix.map(arr => arr.map(item =>
    {item}
    ))}
    ; - } - - render(MyComp, container); - expect(container.innerHTML).toBe('
    1
    2
    3
    4
    '); - }); - - // Compile error - // it.fails('should transform last map to for" ', ({ container }) => { - // function MyComp() { - // let arr = [1, 2, 3]; - // return ( - //
    - // {arr - // .map(item =>
    {item}
    ) - // .map(item => ( - //
    {item}
    - // ))} - //
    - // ); - // } - // - // render(MyComp, container); - // expect(container.innerHTML).toBe('
    1
    2
    3
    4
    '); - // }); - it('Should correctly render a single-level loop of elements', ({ container }) => { - function App() { - const fruits = ['Apple', 'Banana', 'Cherry']; - return {fruit =>
  • {fruit}
  • }
    ; - } - - render(App, container); - expect(container.innerHTML).toMatchInlineSnapshot(`"
  • Apple
  • Banana
  • Cherry
  • "`); - }); - - it('Should correctly render nested loops of elements', ({ container }) => { - function App() { - const matrix = [ - [1, 2], - [3, 4], - [5, 6], - ]; - return ( - - {row => ( -
    - {cell => {cell}} -
    - )} -
    - ); - } - - render(App, container); - expect(container.innerHTML).toMatchInlineSnapshot( - `"
    12
    34
    56
    "` - ); - }); - - it('Should correctly render loops with complex data structures', ({ container }) => { - function App() { - const users = [ - { id: 1, name: 'Alice', hobbies: ['reading', 'gaming'] }, - { id: 2, name: 'Bob', hobbies: ['cycling', 'photography'] }, - ]; - return ( - - {user => ( -
    -

    {user.name}

    -
      - {hobby =>
    • {hobby}
    • }
      -
    -
    - )} -
    - ); - } - - render(App, container); - expect(container.innerHTML).toMatchInlineSnapshot( - `"

    Alice

    • reading
    • gaming

    Bob

    • cycling
    • photography
    "` - ); - }); - - it('Should correctly render when for tag input is an array map', ({ container }) => { - function App() { - const numbers = [1, 2, 3, 4, 5]; - return n * 2)}>{doubledNumber => {doubledNumber}}; - } - - render(App, container); - expect(container.innerHTML).toMatchInlineSnapshot( - `"246810"` - ); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render } from '../src'; + +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); +describe('for', () => { + it('should work', ({ container }) => { + function App() { + const arr = [0, 1, 2]; + return {item =>
    {item}
    }
    ; + } + + render(App, container); + expect(container.innerHTML).toMatchInlineSnapshot(`"
    0
    1
    2
    "`); + }); + + it('should update item when arr changed', ({ container }) => { + let updateArr: (num: number) => void; + + function App() { + const arr = [0, 1, 2]; + updateArr = (num: number) => { + arr.push(num); + }; + return {item =>
    {item}
    }
    ; + } + + render(App, container); + expect(container.children.length).toEqual(3); + updateArr(3); + expect(container.children.length).toEqual(4); + expect(container.innerHTML).toMatchInlineSnapshot(`"
    0
    1
    2
    3
    "`); + }); + + it('should get index', ({ container }) => { + let update: (num: number) => void; + function App() { + const arr = [0, 1, 2]; + update = (num: number) => { + arr.push(num); + }; + return {(item, index) =>
    {index}
    }
    ; + } + + render(App, container); + expect(container.innerHTML).toMatchInlineSnapshot(`"
    0
    1
    2
    "`); + update(3); + expect(container.innerHTML).toMatchInlineSnapshot(`"
    0
    1
    2
    3
    "`); + }); + it('should transform for loop', ({ container }) => { + function MyComp() { + let name = 'test'; + let arr = [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 3 }, + ]; + return ( + + {({ x, y }, index) => { + let name1 = 'test'; + const onClick = () => { + name1 = 'changed'; + }; + return ( +
    + {name1} + {index} +
    + ); + }} +
    + ); + } + + render(MyComp, container); + expect(container.innerHTML).toBe( + '
    test0
    test1
    test2
    ' + ); + const item = container.querySelector('#item0') as HTMLDivElement; + item.click(); + expect(container.innerHTML).toBe( + '
    changed0
    test1
    test2
    ' + ); + }); + + it('should transform map to for jsx element', ({ container }) => { + function MyComp() { + const arr = [1, 2, 3]; + return ( + <> + {arr.map(item => ( +
    {item}
    + ))} + + ); + } + + render(MyComp, container); + expect(container.innerHTML).toBe('
    1
    2
    3
    '); + }); + it('should transform map in map to for', ({ container }) => { + function MyComp() { + const matrix = [ + [1, 2], + [3, 4], + ]; + return
    {matrix.map(arr => arr.map(item =>
    {item}
    ))}
    ; + } + + render(MyComp, container); + expect(container.innerHTML).toBe('
    1
    2
    3
    4
    '); + }); + + // Compile error + // it.fails('should transform last map to for" ', ({ container }) => { + // function MyComp() { + // let arr = [1, 2, 3]; + // return ( + //
    + // {arr + // .map(item =>
    {item}
    ) + // .map(item => ( + //
    {item}
    + // ))} + //
    + // ); + // } + // + // render(MyComp, container); + // expect(container.innerHTML).toBe('
    1
    2
    3
    4
    '); + // }); + it('Should correctly render a single-level loop of elements', ({ container }) => { + function App() { + const fruits = ['Apple', 'Banana', 'Cherry']; + return {fruit =>
  • {fruit}
  • }
    ; + } + + render(App, container); + expect(container.innerHTML).toMatchInlineSnapshot(`"
  • Apple
  • Banana
  • Cherry
  • "`); + }); + + it('Should correctly render nested loops of elements', ({ container }) => { + function App() { + const matrix = [ + [1, 2], + [3, 4], + [5, 6], + ]; + return ( + + {row => ( +
    + {cell => {cell}} +
    + )} +
    + ); + } + + render(App, container); + expect(container.innerHTML).toMatchInlineSnapshot( + `"
    12
    34
    56
    "` + ); + }); + + it('Should correctly render loops with complex data structures', ({ container }) => { + function App() { + const users = [ + { id: 1, name: 'Alice', hobbies: ['reading', 'gaming'] }, + { id: 2, name: 'Bob', hobbies: ['cycling', 'photography'] }, + ]; + return ( + + {user => ( +
    +

    {user.name}

    +
      + {hobby =>
    • {hobby}
    • }
      +
    +
    + )} +
    + ); + } + + render(App, container); + expect(container.innerHTML).toMatchInlineSnapshot( + `"

    Alice

    • reading
    • gaming

    Bob

    • cycling
    • photography
    "` + ); + }); + + it('Should correctly render when for tag input is an array map', ({ container }) => { + function App() { + const numbers = [1, 2, 3, 4, 5]; + return n * 2)}>{doubledNumber => {doubledNumber}}; + } + + render(App, container); + expect(container.innerHTML).toMatchInlineSnapshot( + `"246810"` + ); + }); +}); diff --git a/packages/inula-next/test/fragment.test.tsx b/packages/inula-next/test/fragment.test.tsx index 52780861f6016faeb16032c22afb8f196c9d8a5b..5a79f2d8ea98be7e2c4b858d14ee0da9fbdc9df1 100644 --- a/packages/inula-next/test/fragment.test.tsx +++ b/packages/inula-next/test/fragment.test.tsx @@ -1,209 +1,209 @@ -import { describe, expect, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render } from '../src'; - -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); -describe('Fragment Tests', () => { - it('should render multiple elements using Fragment', ({ container }) => { - function App() { - return ( - <> -
    First
    -
    Second
    - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    First
    Second
    '); - }); - - it('should render an empty Fragment', ({ container }) => { - function App() { - return <>; - } - - render(App, container); - expect(container.innerHTML).toBe(''); - }); - it('should support nested Fragments', ({ container }) => { - function ChildComponent() { - return ( - <> - Child - - ); - } - - function App() { - return ( - <> -
    Start
    - <> -

    Nested

    - - -
    End
    - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    Start

    Nested

    Child
    End
    '); - }); - - it('should support conditional rendering with Fragments', ({ container }) => { - function App() { - let showExtra = false; - - function toggleExtra() { - showExtra = !showExtra; - } - - return ( - <> -
    Always
    - {showExtra && ( - <> -
    Extra 1
    -
    Extra 2
    - - )} - - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    Always
    '); - - container.querySelector('button')?.click(); - expect(container.innerHTML).toBe('
    Always
    Extra 1
    Extra 2
    '); - }); - - it('should support ternary operators with Fragments', ({ container }) => { - function App() { - let condition = true; - - function toggleCondition() { - condition = !condition; - } - - return ( - <> - {condition ? ( - <> -
    True 1
    -
    True 2
    - - ) : ( - <> -
    False 1
    -
    False 2
    - - )} - - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    True 1
    True 2
    '); - - container.querySelector('button')?.click(); - expect(container.innerHTML).toBe('
    False 1
    False 2
    '); - }); - - it('should support state updates within Fragments', ({ container }) => { - function App() { - let count = 0; - - function increment() { - count += 1; - } - - return ( - <> -
    Count: {count}
    - - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    Count: 0
    '); - - container.querySelector('button')?.click(); - expect(container.innerHTML).toBe('
    Count: 1
    '); - }); - - it('should support event handling within Fragments', ({ container }) => { - function App() { - let clicked = false; - - function handleClick() { - clicked = true; - } - - return ( - <> - -
    {clicked ? 'Clicked' : 'Not clicked'}
    - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    Not clicked
    '); - - container.querySelector('button')?.click(); - expect(container.innerHTML).toBe('
    Clicked
    '); - }); - - it('should not affect CSS styling', ({ container }) => { - function App() { - return ( - <> -
    Styled div
    - Red span - - ); - } - - render(App, container); - const styledDiv = container.querySelector('.styled'); - const redSpan = container.querySelector('span'); - - expect(styledDiv).not.toBeNull(); - expect(redSpan).not.toBeNull(); - expect(redSpan?.style.color).toBe('red'); - }); - - it('should support mixing text and JSX expression containers within Fragments', ({ container }) => { - function App() { - const name = 'World'; - const age = 42; - const hobbies = ['reading', 'coding', 'gaming']; - - return ( - <> - Hello, {name}! You are {age} years old.Your hobbies include: - {hobbies.map(h => ( - {h} - ))} - - ); - } - - render(App, container); - expect(container.innerHTML).toBe( - 'Hello, World! You are 42 years old.Your hobbies include:readingcodinggaming' - ); - }); -}); +import { describe, expect, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render } from '../src'; + +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); +describe('Fragment Tests', () => { + it('should render multiple elements using Fragment', ({ container }) => { + function App() { + return ( + <> +
    First
    +
    Second
    + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    First
    Second
    '); + }); + + it('should render an empty Fragment', ({ container }) => { + function App() { + return <>; + } + + render(App, container); + expect(container.innerHTML).toBe(''); + }); + it('should support nested Fragments', ({ container }) => { + function ChildComponent() { + return ( + <> + Child + + ); + } + + function App() { + return ( + <> +
    Start
    + <> +

    Nested

    + + +
    End
    + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    Start

    Nested

    Child
    End
    '); + }); + + it('should support conditional rendering with Fragments', ({ container }) => { + function App() { + let showExtra = false; + + function toggleExtra() { + showExtra = !showExtra; + } + + return ( + <> +
    Always
    + {showExtra && ( + <> +
    Extra 1
    +
    Extra 2
    + + )} + + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    Always
    '); + + container.querySelector('button')?.click(); + expect(container.innerHTML).toBe('
    Always
    Extra 1
    Extra 2
    '); + }); + + it('should support ternary operators with Fragments', ({ container }) => { + function App() { + let condition = true; + + function toggleCondition() { + condition = !condition; + } + + return ( + <> + {condition ? ( + <> +
    True 1
    +
    True 2
    + + ) : ( + <> +
    False 1
    +
    False 2
    + + )} + + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    True 1
    True 2
    '); + + container.querySelector('button')?.click(); + expect(container.innerHTML).toBe('
    False 1
    False 2
    '); + }); + + it('should support state updates within Fragments', ({ container }) => { + function App() { + let count = 0; + + function increment() { + count += 1; + } + + return ( + <> +
    Count: {count}
    + + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    Count: 0
    '); + + container.querySelector('button')?.click(); + expect(container.innerHTML).toBe('
    Count: 1
    '); + }); + + it('should support event handling within Fragments', ({ container }) => { + function App() { + let clicked = false; + + function handleClick() { + clicked = true; + } + + return ( + <> + +
    {clicked ? 'Clicked' : 'Not clicked'}
    + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    Not clicked
    '); + + container.querySelector('button')?.click(); + expect(container.innerHTML).toBe('
    Clicked
    '); + }); + + it('should not affect CSS styling', ({ container }) => { + function App() { + return ( + <> +
    Styled div
    + Red span + + ); + } + + render(App, container); + const styledDiv = container.querySelector('.styled'); + const redSpan = container.querySelector('span'); + + expect(styledDiv).not.toBeNull(); + expect(redSpan).not.toBeNull(); + expect(redSpan?.style.color).toBe('red'); + }); + + it('should support mixing text and JSX expression containers within Fragments', ({ container }) => { + function App() { + const name = 'World'; + const age = 42; + const hobbies = ['reading', 'coding', 'gaming']; + + return ( + <> + Hello, {name}! You are {age} years old.Your hobbies include: + {hobbies.map(h => ( + {h} + ))} + + ); + } + + render(App, container); + expect(container.innerHTML).toBe( + 'Hello, World! You are 42 years old.Your hobbies include:readingcodinggaming' + ); + }); +}); diff --git a/packages/inula-next/test/jsxSlice.test.tsx b/packages/inula-next/test/jsxSlice.test.tsx index cfcf4c5823f10b2afb398824ea62995b6a612d9a..fabfc3d97e4f2a4830af9bc465c0c3001547d7fc 100644 --- a/packages/inula-next/test/jsxSlice.test.tsx +++ b/packages/inula-next/test/jsxSlice.test.tsx @@ -1,679 +1,680 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render, useContext, createContext } from '../src'; - -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); - -describe('JSX Element Usage in Various Contexts', () => { - describe('mount', () => { - it('should support variable assignment of JSX elements', ({ container }) => { - function App() { - const element =
    Hello, World!
    ; - return <>{element}; - } - - render(App, container); - expect(container.innerHTML).toBe('
    Hello, World!
    '); - }); - - it('should support JSX elements in arrays', ({ container }) => { - function App() { - const elements = [
    First
    ,
    Second
    ,
    Third
    ]; - return <>{elements}; - } - - render(App, container); - expect(container.innerHTML).toBe('
    First
    Second
    Third
    '); - }); - - it('should support JSX elements as object properties', ({ container }) => { - function App() { - const obj = { - header:

    Title

    , - content:

    Content

    , - footer:
    Footer
    , - }; - return ( - <> - {obj.header} - {obj.content} - {obj.footer} - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('

    Title

    Content

    Footer
    '); - }); - // - // it('should support functions returning JSX elements', ({ container }) => { - // function App() { - // const getElement = (text: string) => {text}; - // return
    {getElement('Hello')}
    ; - // } - // - // render(App, container); - // expect(container.innerHTML).toBe('
    Hello
    '); - // }); - - it('should support JSX elements in conditional expressions', ({ container }) => { - function App({ condition = true }: { condition: boolean }) { - return ( - <> - {condition ?
    True
    :
    False
    } - {condition &&
    Conditional
    } - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    True
    Conditional
    '); - }); - - it('should support JSX elements as Context Provider values', ({ container }) => { - const ThemeContext = createContext(null); - - function App() { - const theme =
    Dark Theme
    ; - return ( - - - - ); - } - - function ThemeConsumer() { - let { value } = useContext(ThemeContext); - return <>{value}; - } - - render(App, container); - expect(container.innerHTML).toBe('
    Dark Theme
    '); - }); - it('should render string literals', ({ container }) => { - function App() { - return
    {'Hello, World!'}
    ; - } - render(App, container); - expect(container.innerHTML).toBe('
    Hello, World!
    '); - }); - - it('should render numbers', ({ container }) => { - function App() { - return
    {42}
    ; - } - render(App, container); - expect(container.innerHTML).toBe('
    42
    '); - }); - - it.fails('should render booleans (as empty string)', ({ container }) => { - function App() { - return
    {true}
    ; - } - render(App, container); - expect(container.innerHTML).toBe('
    '); - }); - - it.fails('should render null and undefined (as empty string)', ({ container }) => { - function App() { - return ( -
    - {null} - {undefined} -
    - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    '); - }); - - it.fails('should render arrays of elements', ({ container }) => { - function App() { - return
    {[One, Two]}
    ; - } - render(App, container); - expect(container.innerHTML).toBe('
    OneTwo
    '); - }); - - it('should render function components', ({ container }) => { - function Child() { - return Child Component; - } - function App() { - return ( -
    - -
    - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Child Component
    '); - }); - - it('should render fragments', ({ container }) => { - function App() { - return ( - <> - First - Second - - ); - } - render(App, container); - expect(container.innerHTML).toBe('FirstSecond'); - }); - - it('should render elements with props', ({ container }) => { - function App() { - return ( -
    - Content -
    - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Content
    '); - }); - - it('should render elements with children prop', ({ container }) => { - function Wrapper({ children }: { children: React.ReactNode }) { - return
    {children}
    ; - } - function App() { - return ( - - Child Content - - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Child Content
    '); - }); - - it('should correctly render nested HTML elements', ({ container }) => { - function App() { - return ( -
    -

    - Nested content -

    -
    - ); - } - render(App, container); - expect(container.innerHTML).toBe('

    Nested content

    '); - }); - - it('should correctly render a mix of HTML elements and text', ({ container }) => { - function App() { - return ( -
    - Text before Element Text after -
    - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Text before Element Text after
    '); - }); - - it('should correctly render text on both sides of an element', ({ container }) => { - function App() { - return ( -
    - Left side text Bold text Right side text -
    - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Left side text Bold text Right side text
    '); - }); - - it('should correctly render a mix of curly braces, text, and elements in different orders', ({ container }) => { - function App() { - const name = 'World'; - return ( -
    - {/* Curly braces, then text, then element */} - {name}, Hello ! - {/* Element, then curly braces, then text */} - Greetings {name} to you - {/* Text, then element, then curly braces */} - Welcome dear {name} -
    - ); - } - render(App, container); - expect(container.innerHTML).toBe( - '
    World, Hello !Greetings World to you' + 'Welcome dear World
    ' - ); - }); - }); - describe('update', () => { - it('should support variable assignment of JSX elements and updates', ({ container }) => { - function App() { - let element =
    Hello, World!
    ; - - function updateElement() { - element =
    Updated World!
    ; - } - - return ( - <> - {element} - - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    Hello, World!
    '); - - container.querySelector('button')?.click(); - expect(container.innerHTML).toBe('
    Updated World!
    '); - }); - - it('should support JSX elements in arrays with updates', ({ container }) => { - function App() { - let elements = [
    First
    ,
    Second
    ,
    Third
    ]; - - function updateElements() { - elements = [
    Updated First
    ,
    Updated Second
    ,
    Updated Third
    ]; - } - - return ( - <> - {elements} - - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    First
    Second
    Third
    '); - - container.querySelector('button')?.click(); - expect(container.innerHTML).toBe( - '
    Updated First
    Updated Second
    Updated Third
    ' - ); - }); - - it('should support JSX elements as object properties with updates', ({ container }) => { - function App() { - let obj = { - header:

    Title

    , - content:

    Content

    , - footer:
    Footer
    , - }; - - function updateObj() { - obj = { - header:

    Updated Title

    , - content:

    Updated Content

    , - footer:
    Updated Footer
    , - }; - } - - return ( - <> - {obj.header} - {obj.content} - {obj.footer} - - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('

    Title

    Content

    Footer
    '); - - container.querySelector('button')?.click(); - expect(container.innerHTML).toBe( - '

    Updated Title

    Updated Content

    Updated Footer
    ' - ); - }); - - // it('should support functions returning JSX elements with updates', ({ container }) => { - // function App() { - // let text = 'Hello'; - // - // const getElement = (t: string) => {t}; - // - // function updateText() { - // text = 'Updated Hello'; - // } - // - // return ( - //
    - // {getElement(text)} - // - //
    - // ); - // } - // - // render(App, container); - // expect(container.innerHTML).toBe('
    Hello
    '); - // - // container.querySelector('button')?.click(); - // expect(container.innerHTML).toBe('
    Updated Hello
    '); - // }); - - it('should support JSX elements in conditional expressions with updates', ({ container }) => { - function App() { - let condition = true; - - function toggleCondition() { - condition = !condition; - } - - return ( - <> - {condition ?
    True
    :
    False
    } - {condition &&
    Conditional
    } - - - ); - } - - render(App, container); - expect(container.innerHTML).toBe('
    True
    Conditional
    '); - - container.querySelector('button')?.click(); - expect(container.innerHTML).toBe('
    False
    '); - }); - }); -}); - -describe('JSX Element Attributes', () => { - it('should correctly initialize attributes', ({ container }) => { - function App() { - return ( -
    - Content -
    - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Content
    '); - }); - - it('should correctly update attributes', ({ container }) => { - function App() { - let className = 'initial'; - - function updateClass() { - className = 'updated'; - } - - return ( - <> -
    Content
    - - - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Content
    '); - - container.querySelector('button')?.click(); - expect(container.innerHTML).toBe('
    Content
    '); - }); - - it('should correctly render attributes dependent on variables', ({ container }) => { - function App() { - let className = 'initial'; - let b = className; - function updateClass() { - className = 'updated'; - } - - return ( - <> -
    Content
    - - - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Content
    '); - - container.querySelector('button')?.click(); - expect(container.innerHTML).toBe('
    Content
    '); - }); - - it('should correctly render attributes with expressions', ({ container }) => { - function App() { - const count = 5; - return
    Content
    ; - } - render(App, container); - expect(container.innerHTML).toBe('
    Content
    '); - }); - - it('should correctly render boolean attributes', ({ container }) => { - function App() { - const disabled = true; - return ; - } - render(App, container); - expect(container.innerHTML).toBe(''); - }); - - it.fails('should correctly render attributes without values', ({ container }) => { - function App() { - const checked = true; - return ; - } - render(App, container); - expect(container.innerHTML).toBe(''); - }); - - it.fails('should correctly spread multiple attributes', ({ container }) => { - function App() { - const props = { - id: 'test-id', - className: 'test-class', - 'data-test': 'test-data', - }; - return
    Content
    ; - } - render(App, container); - expect(container.innerHTML).toBe('
    Content
    '); - }); - - it.fails('should correctly handle attribute spreading and individual props', ({ container }) => { - function App() { - const props = { - id: 'base-id', - className: 'base-class', - }; - return ( -
    - Content -
    - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Content
    '); - }); -}); - -describe('JSX Element Inline Styles', () => { - it('should correctly apply inline styles to an element', ({ container }) => { - function App() { - return
    Styled content
    ; - } - render(App, container); - expect(container.innerHTML).toBe('
    Styled content
    '); - }); - - it('should correctly apply multiple inline styles to an element', ({ container }) => { - function App() { - return
    Multiple styles
    ; - } - render(App, container); - expect(container.innerHTML).toBe( - '
    Multiple styles
    ' - ); - }); - - it('should correctly apply styles from a variable', ({ container }) => { - function App() { - const styleObj = { color: 'green', padding: '5px' }; - return
    Variable style
    ; - } - render(App, container); - expect(container.innerHTML).toBe('
    Variable style
    '); - }); - - it('should correctly update styles', ({ container }) => { - function App() { - let style = { color: 'purple' }; - - function updateStyle() { - style = { color: 'orange', fontSize: '24px' }; - } - - return ( - <> -
    Updatable style
    - - - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Updatable style
    '); - - container.querySelector('button')?.click(); - expect(container.innerHTML).toBe( - '
    Updatable style
    ' - ); - }); - - it('should correctly apply styles from an expression', ({ container }) => { - function App() { - const size = 18; - return
    Expression style
    ; - } - render(App, container); - expect(container.innerHTML).toBe('
    Expression style
    '); - }); - - it('should correctly merge style objects', ({ container }) => { - function App() { - const baseStyle = { color: 'red', fontSize: '16px' }; - const additionalStyle = { fontSize: '20px', fontWeight: 'bold' }; - return
    Merged styles
    ; - } - render(App, container); - expect(container.innerHTML).toBe( - '
    Merged styles
    ' - ); - }); - - it('should correctly apply styles from a function call', ({ container }) => { - function getStyles(color: string) { - return { color, border: `1px solid ${color}` }; - } - function App() { - return
    Function style
    ; - } - render(App, container); - expect(container.innerHTML).toBe('
    Function style
    '); - }); - - it('should correctly apply styles based on a condition', ({ container }) => { - function App() { - const isActive = true; - const style = isActive - ? { backgroundColor: 'green', color: 'white' } - : { backgroundColor: 'gray', color: 'black' }; - return
    Conditional style
    ; - } - render(App, container); - expect(container.innerHTML).toBe('
    Conditional style
    '); - }); - - it('should correctly apply styles based on an array of conditions', ({ container }) => { - function App() { - const conditions = [true, false, true]; - const style = { - color: conditions[0] ? 'red' : 'blue', - fontWeight: conditions[1] ? 'bold' : 'normal', - fontSize: conditions[2] ? '20px' : '16px', - }; - return
    Array condition style
    ; - } - render(App, container); - expect(container.innerHTML).toBe( - '
    Array condition style
    ' - ); - }); - - it('should correctly apply styles using ternary and binary expressions', ({ container }) => { - function App() { - const isPrimary = true; - const isLarge = true; - return ( -
    - Ternary and binary style -
    - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Ternary and binary style
    '); - }); - - it('should correctly apply styles using member expressions', ({ container }) => { - const theme = { - colors: { - primary: 'blue', - secondary: 'green', - }, - sizes: { - small: '12px', - medium: '16px', - large: '20px', - }, - }; - function App() { - return ( -
    - Member expression style -
    - ); - } - render(App, container); - expect(container.innerHTML).toBe('
    Member expression style
    '); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render } from '../src'; +import { useContext, createContext } from '../src/ContextNode'; + +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); + +describe('JSX Element Usage in Various Contexts', () => { + describe('mount', () => { + it('should support variable assignment of JSX elements', ({ container }) => { + function App() { + const element =
    Hello, World!
    ; + return <>{element}; + } + + render(App, container); + expect(container.innerHTML).toBe('
    Hello, World!
    '); + }); + + it('should support JSX elements in arrays', ({ container }) => { + function App() { + const elements = [
    First
    ,
    Second
    ,
    Third
    ]; + return <>{elements}; + } + + render(App, container); + expect(container.innerHTML).toBe('
    First
    Second
    Third
    '); + }); + + it('should support JSX elements as object properties', ({ container }) => { + function App() { + const obj = { + header:

    Title

    , + content:

    Content

    , + footer:
    Footer
    , + }; + return ( + <> + {obj.header} + {obj.content} + {obj.footer} + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('

    Title

    Content

    Footer
    '); + }); + // + // it('should support functions returning JSX elements', ({ container }) => { + // function App() { + // const getElement = (text: string) => {text}; + // return
    {getElement('Hello')}
    ; + // } + // + // render(App, container); + // expect(container.innerHTML).toBe('
    Hello
    '); + // }); + + it('should support JSX elements in conditional expressions', ({ container }) => { + function App({ condition = true }: { condition: boolean }) { + return ( + <> + {condition ?
    True
    :
    False
    } + {condition &&
    Conditional
    } + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    True
    Conditional
    '); + }); + + it('should support JSX elements as Context Provider values', ({ container }) => { + const ThemeContext = createContext(null); + + function App() { + const theme =
    Dark Theme
    ; + return ( + + + + ); + } + + function ThemeConsumer() { + let { value } = useContext(ThemeContext); + return <>{value}; + } + + render(App, container); + expect(container.innerHTML).toBe('
    Dark Theme
    '); + }); + it('should render string literals', ({ container }) => { + function App() { + return
    {'Hello, World!'}
    ; + } + render(App, container); + expect(container.innerHTML).toBe('
    Hello, World!
    '); + }); + + it('should render numbers', ({ container }) => { + function App() { + return
    {42}
    ; + } + render(App, container); + expect(container.innerHTML).toBe('
    42
    '); + }); + + it.fails('should render booleans (as empty string)', ({ container }) => { + function App() { + return
    {true}
    ; + } + render(App, container); + expect(container.innerHTML).toBe('
    '); + }); + + it.fails('should render null and undefined (as empty string)', ({ container }) => { + function App() { + return ( +
    + {null} + {undefined} +
    + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    '); + }); + + it.fails('should render arrays of elements', ({ container }) => { + function App() { + return
    {[One, Two]}
    ; + } + render(App, container); + expect(container.innerHTML).toBe('
    OneTwo
    '); + }); + + it('should render function components', ({ container }) => { + function Child() { + return Child Component; + } + function App() { + return ( +
    + +
    + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Child Component
    '); + }); + + it('should render fragments', ({ container }) => { + function App() { + return ( + <> + First + Second + + ); + } + render(App, container); + expect(container.innerHTML).toBe('FirstSecond'); + }); + + it('should render elements with props', ({ container }) => { + function App() { + return ( +
    + Content +
    + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Content
    '); + }); + + it('should render elements with children prop', ({ container }) => { + function Wrapper({ children }: { children: React.ReactNode }) { + return
    {children}
    ; + } + function App() { + return ( + + Child Content + + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Child Content
    '); + }); + + it('should correctly render nested HTML elements', ({ container }) => { + function App() { + return ( +
    +

    + Nested content +

    +
    + ); + } + render(App, container); + expect(container.innerHTML).toBe('

    Nested content

    '); + }); + + it('should correctly render a mix of HTML elements and text', ({ container }) => { + function App() { + return ( +
    + Text before Element Text after +
    + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Text before Element Text after
    '); + }); + + it('should correctly render text on both sides of an element', ({ container }) => { + function App() { + return ( +
    + Left side text Bold text Right side text +
    + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Left side text Bold text Right side text
    '); + }); + + it('should correctly render a mix of curly braces, text, and elements in different orders', ({ container }) => { + function App() { + const name = 'World'; + return ( +
    + {/* Curly braces, then text, then element */} + {name}, Hello ! + {/* Element, then curly braces, then text */} + Greetings {name} to you + {/* Text, then element, then curly braces */} + Welcome dear {name} +
    + ); + } + render(App, container); + expect(container.innerHTML).toBe( + '
    World, Hello !Greetings World to you' + 'Welcome dear World
    ' + ); + }); + }); + describe('update', () => { + it('should support variable assignment of JSX elements and updates', ({ container }) => { + function App() { + let element =
    Hello, World!
    ; + + function updateElement() { + element =
    Updated World!
    ; + } + + return ( + <> + {element} + + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    Hello, World!
    '); + + container.querySelector('button')?.click(); + expect(container.innerHTML).toBe('
    Updated World!
    '); + }); + + it('should support JSX elements in arrays with updates', ({ container }) => { + function App() { + let elements = [
    First
    ,
    Second
    ,
    Third
    ]; + + function updateElements() { + elements = [
    Updated First
    ,
    Updated Second
    ,
    Updated Third
    ]; + } + + return ( + <> + {elements} + + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    First
    Second
    Third
    '); + + container.querySelector('button')?.click(); + expect(container.innerHTML).toBe( + '
    Updated First
    Updated Second
    Updated Third
    ' + ); + }); + + it('should support JSX elements as object properties with updates', ({ container }) => { + function App() { + let obj = { + header:

    Title

    , + content:

    Content

    , + footer:
    Footer
    , + }; + + function updateObj() { + obj = { + header:

    Updated Title

    , + content:

    Updated Content

    , + footer:
    Updated Footer
    , + }; + } + + return ( + <> + {obj.header} + {obj.content} + {obj.footer} + + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('

    Title

    Content

    Footer
    '); + + container.querySelector('button')?.click(); + expect(container.innerHTML).toBe( + '

    Updated Title

    Updated Content

    Updated Footer
    ' + ); + }); + + // it('should support functions returning JSX elements with updates', ({ container }) => { + // function App() { + // let text = 'Hello'; + // + // const getElement = (t: string) => {t}; + // + // function updateText() { + // text = 'Updated Hello'; + // } + // + // return ( + //
    + // {getElement(text)} + // + //
    + // ); + // } + // + // render(App, container); + // expect(container.innerHTML).toBe('
    Hello
    '); + // + // container.querySelector('button')?.click(); + // expect(container.innerHTML).toBe('
    Updated Hello
    '); + // }); + + it('should support JSX elements in conditional expressions with updates', ({ container }) => { + function App() { + let condition = true; + + function toggleCondition() { + condition = !condition; + } + + return ( + <> + {condition ?
    True
    :
    False
    } + {condition &&
    Conditional
    } + + + ); + } + + render(App, container); + expect(container.innerHTML).toBe('
    True
    Conditional
    '); + + container.querySelector('button')?.click(); + expect(container.innerHTML).toBe('
    False
    '); + }); + }); +}); + +describe('JSX Element Attributes', () => { + it('should correctly initialize attributes', ({ container }) => { + function App() { + return ( +
    + Content +
    + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Content
    '); + }); + + it('should correctly update attributes', ({ container }) => { + function App() { + let className = 'initial'; + + function updateClass() { + className = 'updated'; + } + + return ( + <> +
    Content
    + + + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Content
    '); + + container.querySelector('button')?.click(); + expect(container.innerHTML).toBe('
    Content
    '); + }); + + it('should correctly render attributes dependent on variables', ({ container }) => { + function App() { + let className = 'initial'; + let b = className; + function updateClass() { + className = 'updated'; + } + + return ( + <> +
    Content
    + + + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Content
    '); + + container.querySelector('button')?.click(); + expect(container.innerHTML).toBe('
    Content
    '); + }); + + it('should correctly render attributes with expressions', ({ container }) => { + function App() { + const count = 5; + return
    Content
    ; + } + render(App, container); + expect(container.innerHTML).toBe('
    Content
    '); + }); + + it('should correctly render boolean attributes', ({ container }) => { + function App() { + const disabled = true; + return ; + } + render(App, container); + expect(container.innerHTML).toBe(''); + }); + + it.fails('should correctly render attributes without values', ({ container }) => { + function App() { + const checked = true; + return ; + } + render(App, container); + expect(container.innerHTML).toBe(''); + }); + + it.fails('should correctly spread multiple attributes', ({ container }) => { + function App() { + const props = { + id: 'test-id', + className: 'test-class', + 'data-test': 'test-data', + }; + return
    Content
    ; + } + render(App, container); + expect(container.innerHTML).toBe('
    Content
    '); + }); + + it.fails('should correctly handle attribute spreading and individual props', ({ container }) => { + function App() { + const props = { + id: 'base-id', + className: 'base-class', + }; + return ( +
    + Content +
    + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Content
    '); + }); +}); + +describe('JSX Element Inline Styles', () => { + it('should correctly apply inline styles to an element', ({ container }) => { + function App() { + return
    Styled content
    ; + } + render(App, container); + expect(container.innerHTML).toBe('
    Styled content
    '); + }); + + it('should correctly apply multiple inline styles to an element', ({ container }) => { + function App() { + return
    Multiple styles
    ; + } + render(App, container); + expect(container.innerHTML).toBe( + '
    Multiple styles
    ' + ); + }); + + it('should correctly apply styles from a variable', ({ container }) => { + function App() { + const styleObj = { color: 'green', padding: '5px' }; + return
    Variable style
    ; + } + render(App, container); + expect(container.innerHTML).toBe('
    Variable style
    '); + }); + + it('should correctly update styles', ({ container }) => { + function App() { + let style = { color: 'purple' }; + + function updateStyle() { + style = { color: 'orange', fontSize: '24px' }; + } + + return ( + <> +
    Updatable style
    + + + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Updatable style
    '); + + container.querySelector('button')?.click(); + expect(container.innerHTML).toBe( + '
    Updatable style
    ' + ); + }); + + it('should correctly apply styles from an expression', ({ container }) => { + function App() { + const size = 18; + return
    Expression style
    ; + } + render(App, container); + expect(container.innerHTML).toBe('
    Expression style
    '); + }); + + it('should correctly merge style objects', ({ container }) => { + function App() { + const baseStyle = { color: 'red', fontSize: '16px' }; + const additionalStyle = { fontSize: '20px', fontWeight: 'bold' }; + return
    Merged styles
    ; + } + render(App, container); + expect(container.innerHTML).toBe( + '
    Merged styles
    ' + ); + }); + + it('should correctly apply styles from a function call', ({ container }) => { + function getStyles(color: string) { + return { color, border: `1px solid ${color}` }; + } + function App() { + return
    Function style
    ; + } + render(App, container); + expect(container.innerHTML).toBe('
    Function style
    '); + }); + + it('should correctly apply styles based on a condition', ({ container }) => { + function App() { + const isActive = true; + const style = isActive + ? { backgroundColor: 'green', color: 'white' } + : { backgroundColor: 'gray', color: 'black' }; + return
    Conditional style
    ; + } + render(App, container); + expect(container.innerHTML).toBe('
    Conditional style
    '); + }); + + it('should correctly apply styles based on an array of conditions', ({ container }) => { + function App() { + const conditions = [true, false, true]; + const style = { + color: conditions[0] ? 'red' : 'blue', + fontWeight: conditions[1] ? 'bold' : 'normal', + fontSize: conditions[2] ? '20px' : '16px', + }; + return
    Array condition style
    ; + } + render(App, container); + expect(container.innerHTML).toBe( + '
    Array condition style
    ' + ); + }); + + it('should correctly apply styles using ternary and binary expressions', ({ container }) => { + function App() { + const isPrimary = true; + const isLarge = true; + return ( +
    + Ternary and binary style +
    + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Ternary and binary style
    '); + }); + + it('should correctly apply styles using member expressions', ({ container }) => { + const theme = { + colors: { + primary: 'blue', + secondary: 'green', + }, + sizes: { + small: '12px', + medium: '16px', + large: '20px', + }, + }; + function App() { + return ( +
    + Member expression style +
    + ); + } + render(App, container); + expect(container.innerHTML).toBe('
    Member expression style
    '); + }); +}); diff --git a/packages/inula-next/test/lifecycle.test.tsx b/packages/inula-next/test/lifecycle.test.tsx index 411a364a5d4e3ad2f72d8c050c00f75f29857e3e..77ce76e91152ae49fe6b7418ac050cfcd5231733 100644 --- a/packages/inula-next/test/lifecycle.test.tsx +++ b/packages/inula-next/test/lifecycle.test.tsx @@ -1,112 +1,184 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render, didMount, willMount, didUnmount } from '../src'; - -describe('lifecycle', () => { - it('should call willMount', ({ container }) => { - const fn = vi.fn(); - - function App() { - willMount(() => { - expect(container.innerHTML).toBe(''); - fn(); - }); - - return
    test
    ; - } - - render(App, container); - expect(fn).toHaveBeenCalled(); - }); - - it('should call didMount', ({ container }) => { - const fn = vi.fn(); - - function App() { - didMount(() => { - expect(container.innerHTML).toBe('
    test
    '); - fn(); - }); - - return
    test
    ; - } - - render(App, container); - expect(fn).toHaveBeenCalled(); - }); - - it('should handle async operations in didMount', async ({ container }) => { - function App() { - let users: string[] = []; - - didMount(async () => { - await new Promise(resolve => setTimeout(resolve, 0)); - users = ['Alice', 'Bob']; - }); - - return
    {users.join(', ')}
    ; - } - - vi.useFakeTimers(); - render(App, container); - expect(container.innerHTML).toBe('
    '); - - await vi.runAllTimersAsync(); - expect(container.innerHTML).toBe('
    Alice, Bob
    '); - }); - - it('should handle async errors in didMount with try-catch', async ({ container }) => { - function App() { - let text = 'initial'; - - didMount(async () => { - try { - await new Promise((_, reject) => setTimeout(() => reject(new Error('Async error')), 0)); - } catch (error) { - text = 'Error caught'; - } - }); - - return
    {text}
    ; - } - - vi.useFakeTimers(); - render(App, container); - expect(container.innerHTML).toBe('
    initial
    '); - - await vi.runAllTimersAsync(); - expect(container.innerHTML).toBe('
    Error caught
    '); - }); - - // TODO: implement unmount - it.fails('should call willUnmount', ({ container }) => { - const fn = vi.fn(); - - function App() { - didUnmount(() => { - expect(container.innerHTML).toBe('
    test
    '); - fn(); - }); - - return
    test
    ; - } - - render(App, container); - expect(fn).toHaveBeenCalled(); - }); -}); +/* + * 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, vi, beforeEach } from 'vitest'; +import { domTest as it } from './utils'; +import { render, didMount, willMount, didUnmount, willUnmount } from '../src'; +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); + +describe('lifecycle', () => { + it('should call willMount', ({ container }) => { + const fn = vi.fn(); + + function App() { + willMount(() => { + expect(container.innerHTML).toBe(''); + fn(); + }); + + return
    test
    ; + } + + render(App, container); + expect(fn).toHaveBeenCalled(); + }); + + it('should call didMount', ({ container }) => { + const fn = vi.fn(); + + function App() { + didMount(() => { + expect(container.innerHTML).toBe('
    test
    '); + fn(); + }); + + return
    test
    ; + } + + render(App, container); + expect(fn).toHaveBeenCalled(); + }); + + it('should handle async operations in didMount', async ({ container }) => { + function App() { + let users: string[] = []; + + didMount(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + users = ['Alice', 'Bob']; + }); + + return
    {users.join(', ')}
    ; + } + + vi.useFakeTimers(); + render(App, container); + expect(container.innerHTML).toBe('
    '); + + await vi.runAllTimersAsync(); + expect(container.innerHTML).toBe('
    Alice, Bob
    '); + }); + + it('should handle async errors in didMount with try-catch', async ({ container }) => { + function App() { + let text = 'initial'; + + didMount(async () => { + try { + await new Promise((_, reject) => setTimeout(() => reject(new Error('Async error')), 0)); + } catch (error) { + text = 'Error caught'; + } + }); + + return
    {text}
    ; + } + + vi.useFakeTimers(); + render(App, container); + expect(container.innerHTML).toBe('
    initial
    '); + + await vi.runAllTimersAsync(); + expect(container.innerHTML).toBe('
    Error caught
    '); + }); + + describe('willUnmount', () => { + // TODO: implement unmount + it.fails('should call willUnmount', ({ container }) => { + const fn = vi.fn(); + + function App() { + didUnmount(() => { + expect(container.innerHTML).toBe('
    test
    '); + fn(); + }); + + return
    test
    ; + } + + render(App, container); + expect(fn).toHaveBeenCalled(); + }); + + const fn = vi.fn(); + + function Child() { + willUnmount(() => { + fn(); + }); + + return
    test
    ; + } + + beforeEach(() => { + fn.mockClear(); + }); + + it('should call willUnmount in if condition changed', ({ container }) => { + let setCond: (cond: boolean) => void; + function App() { + let cond = true; + setCond = (value: boolean) => { + cond = value; + }; + return ( + + + + ); + } + + render(App, container); + setCond!(false); + expect(fn).toHaveBeenCalled(); + }); + + it('should call willUnmount in expression updated', ({ container }) => { + let setCond: (cond: boolean) => void; + function App() { + let cond = true; + setCond = (value: boolean) => { + cond = value; + }; + return
    {cond ? : null}
    ; + } + + render(App, container); + setCond!(false); + expect(fn).toHaveBeenCalled(); + }); + + it('should call willUnmount in for', ({ container }) => { + let setArr: (arr: string[]) => void; + function App() { + let arr = ['a', 'b', 'c']; + setArr = (value: string[]) => { + arr = value; + }; + return {item => }; + } + + render(App, container); + setArr!([]); + expect(fn).toHaveBeenCalledTimes(3); + }); + }); +}); diff --git a/packages/inula-next/test/props.test.tsx b/packages/inula-next/test/props.test.tsx index c5c7c33ee6a5e15b7664f6e3680e65a755e44212..6b2e8cba577b937d9db9aed1fd4664916e2c5dbd 100644 --- a/packages/inula-next/test/props.test.tsx +++ b/packages/inula-next/test/props.test.tsx @@ -1,368 +1,328 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render, View } from '../src'; - -describe('props', () => { - describe('normal props', () => { - it('should support prop', ({ container }) => { - function Child({ name }) { - return

    {name}

    ; - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -

    - hello world!!! -

    -
    - `); - }); - - it('should support prop alias', ({ container }) => { - function Child({ name: alias }) { - return

    {alias}

    ; - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -

    - prop alias -

    -
    - `); - }); - - it('should support prop alias with default value', ({ container }) => { - function Child({ name: alias = 'default' }) { - return

    {alias}

    ; - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -

    - default -

    -
    - `); - }); - }); - - describe('children', () => { - it('should support children', ({ container }) => { - function Child({ children }) { - return

    {children}

    ; - } - - function App() { - return child content; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -

    - child content -

    -
    - `); - }); - - it('should support children alias', ({ container }) => { - function Child({ children: alias }) { - return

    {alias}

    ; - } - - function App() { - return children alias; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -

    - children alias -

    -
    - `); - }); - - it('should support children alias with default value', ({ container }) => { - function Child({ children: alias = 'default child' }) { - return

    {alias}

    ; - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -

    - default child -

    -
    - `); - }); - }); -}); - -describe('extended prop tests', () => { - it('should correctly pass and render string props', ({ container }) => { - function Child({ text }) { - return

    {text}

    ; - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -

    - Hello, world! -

    -
    - `); - }); - - it('should correctly pass and render number props', ({ container }) => { - function Child({ number }) { - return {number}; - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    - - 42 - -
    - `); - }); - - it('should correctly pass and render boolean props', ({ container }) => { - function Child({ isActive }) { - return
    {isActive ? 'Active' : 'Inactive'}
    ; - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
    - Active -
    -
    - `); - }); - - it('should correctly pass and render array props', ({ container }) => { - function Child({ items }) { - return ( -
      - {items.map((item, index) => ( -
    • {item}
    • - ))} -
    - ); - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
      -
    • - Apple -
    • -
    • - Banana -
    • -
    • - Cherry -
    • -
    -
    - `); - }); - - it('should correctly pass and render object props', ({ container }) => { - function Child({ person }) { - return ( -
    - {person.name},{person.age} -
    - ); - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
    - Alice - , - 30 -
    -
    - `); - }); - - it('should correctly handle default prop values', ({ container }) => { - function Child({ message = 'Default message' }) { - return

    {message}

    ; - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -

    - Default message -

    -
    - `); - }); - - it('should correctly spread props', ({ container }) => { - function Child(props) { - return ( -
    - {props.a} - {props.b} - {props.c} -
    - ); - } - - function App() { - const extraProps = { b: 'World', c: '!' }; - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
    - Hello - World - ! -
    -
    - `); - }); - - it('should support member prop', ({ container }) => { - function Child(props) { - return
    {props.a}
    ; - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
    - Hello -
    -
    - `); - }); - - it('should handle props without values', ({ container }) => { - function Child({ isDisabled }) { - return ; - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    - -
    - `); - }); - - it('should handle props with expressions', ({ container }) => { - function Child({ result }) { - return
    {result}
    ; - } - - function App() { - return ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
    - 7 -
    -
    - `); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render, View } from '../src'; + +describe('props', () => { + describe('normal props', () => { + it('should support prop', ({ container }) => { + function Child({ name }) { + return

    {name}

    ; + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +

    + hello world!!! +

    +
    + `); + }); + + it('should support prop alias', ({ container }) => { + function Child({ name: alias }) { + return

    {alias}

    ; + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +

    + prop alias +

    +
    + `); + }); + + it('should support prop alias with default value', ({ container }) => { + function Child({ name: alias = 'default' }) { + return

    {alias}

    ; + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +

    + default +

    +
    + `); + }); + }); + + describe('children', () => { + it('should support children', ({ container }) => { + function Child({ children }) { + return

    {children}

    ; + } + + function App() { + return child content; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +

    + child content +

    +
    + `); + }); + + it('should support children alias', ({ container }) => { + function Child({ children: alias }) { + return

    {alias}

    ; + } + + function App() { + return children alias; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +

    + children alias +

    +
    + `); + }); + + it('should support children alias with default value', ({ container }) => { + function Child({ children: alias = 'default child' }) { + return

    {alias}

    ; + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +

    + default child +

    +
    + `); + }); + }); +}); + +describe('extended prop tests', () => { + it('should correctly pass and render string props', ({ container }) => { + function Child({ text }) { + return

    {text}

    ; + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +

    + Hello, world! +

    +
    + `); + }); + + it('should correctly pass and render number props', ({ container }) => { + function Child({ number }) { + return {number}; + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    + + 42 + +
    + `); + }); + + it('should correctly pass and render boolean props', ({ container }) => { + function Child({ isActive }) { + return
    {isActive ? 'Active' : 'Inactive'}
    ; + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
    + Active +
    +
    + `); + }); + + it.fails('should correctly pass and render array props', ({ container }) => { + function Child({ items }) { + return ( +
      + {items.map((item, index) => ( +
    • {item}
    • + ))} +
    + ); + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
      +
    • Apple
    • +
    • Banana
    • +
    • Cherry
    • +
    +
    + `); + }); + + it.fails('should correctly pass and render object props', ({ container }) => { + function Child({ person }) { + return ( +
    + {person.name}, {person.age} +
    + ); + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
    + Alice, 30 +
    +
    + `); + }); + + it('should correctly handle default prop values', ({ container }) => { + function Child({ message = 'Default message' }) { + return

    {message}

    ; + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +

    + Default message +

    +
    + `); + }); + + it.fails('should correctly spread props', ({ container }) => { + function Child(props) { + return ( +
    + {props.a} {props.b} {props.c} +
    + ); + } + + function App() { + const extraProps = { b: 'World', c: '!' }; + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
    + Hello World ! +
    +
    + `); + }); + + it.fails('should handle props without values', ({ container }) => { + function Child({ isDisabled }) { + return ; + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    + +
    + `); + }); + + it('should handle props with expressions', ({ container }) => { + function Child({ result }) { + return
    {result}
    ; + } + + function App() { + return ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
    + 7 +
    +
    + `); + }); +}); diff --git a/packages/inula-next/test/rendering.test.tsx b/packages/inula-next/test/rendering.test.tsx index cec13b0a6482dc6bb5bd7af002d48ccaa0bf52e3..4dc37b3ec8b4faae81964d3af94507fa9b154498 100644 --- a/packages/inula-next/test/rendering.test.tsx +++ b/packages/inula-next/test/rendering.test.tsx @@ -1,320 +1,322 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render, View } from '../src'; - -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); - -describe('rendering', () => { - describe('basic', () => { - it('should support basic dom', ({ container }) => { - function App() { - return

    hello world!!!

    ; - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -

    - hello world!!! -

    -
    - `); - }); - - it('should support text and variable mixing', ({ container }) => { - function App() { - const name = 'world'; - return

    hello {name}!!!

    ; - } - - render(App, container); - expect(container.innerHTML).toBe('

    hello world!!!

    '); - }); - - it('should support dom has multiple layers ', ({ container }) => { - function App() { - let count = 0; - return ( -
    - Header -

    hello world!!!

    -
    - -
    - Footer -
    - ); - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
    - Header -

    - hello world!!! -

    -
    - -
    - Footer -
    -
    - `); - }); - - it('should support tag, text and variable mixing', ({ container }) => { - function App() { - let count = 'world'; - - return ( -
    - count:{count} - -
    - ); - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
    - count: - world - -
    -
    - `); - }); - }); - - describe('style', () => { - it('should apply styles correctly', ({ container }) => { - function App() { - return

    hello world!!!

    ; - } - - render(App, container); - const h1 = container.querySelector('h1'); - expect(h1.style.color).toBe('red'); - }); - - it('should apply styles number correctly', ({ container }) => { - function App() { - return
    hello world!!!
    ; - } - - render(App, container); - const div = container.querySelector('div'); - expect(div.style.fontSize).toBe('20px'); - }); - - it('should apply styles empty correctly', ({ container }) => { - function App() { - return

    hello world!!!

    ; - } - - render(App, container); - const h1 = container.querySelector('h1'); - expect(h1.style.fontSize).toBe(''); - }); - - it('should apply multiple styles correctly', ({ container }) => { - function App() { - return

    hello world!!!

    ; - } - - render(App, container); - const h1 = container.querySelector('h1'); - expect(h1.style.color).toBe('red'); - expect(h1.style.fontSize).toBe('20px'); - }); - - it('should override styles correctly', ({ container }) => { - function App() { - return ( -

    - hello world!!! -

    - ); - } - - render(App, container); - const span = container.querySelector('span'); - expect(span.style.color).toBe('blue'); - }); - - it('should handle dynamic styles', ({ container }) => { - const color = 'red'; - - function App() { - return

    hello world!!!

    ; - } - - render(App, container); - const h1 = container.querySelector('h1'); - expect(h1.style.color).toBe('red'); - }); - - it('should handle style object', ({ container }) => { - const color = 'red'; - - function App() { - const style = { color }; - return

    hello world!!!

    ; - } - - render(App, container); - const h1 = container.querySelector('h1'); - expect(h1.style.color).toBe('red'); - }); - - it('should update style', ({ container }) => { - function App() { - let color = 'red'; - return ( -

    (color = 'blue')}> - hello world!!! -

    - ); - } - - render(App, container); - const h1 = container.querySelector('h1'); - expect(h1.style.color).toBe('red'); - h1.click(); - expect(h1.style.color).toBe('blue'); - }); - - it('should update when object style changed', ({ container }) => { - function App() { - let style = { color: 'red', fontSize: '20px' }; - return ( -
    { - style = { color: 'blue', height: '20px' }; - }} - > - hello world!!! -
    - ); - } - - render(App, container); - const div = container.querySelector('div'); - expect(div.style.color).toBe('red'); - expect(div.style.fontSize).toBe('20px'); - expect(div.style.height).toBe(''); - div.click(); - expect(div.style.color).toBe('blue'); - expect(div.style.height).toBe('20px'); - expect(div.style.fontSize).toBe(''); - }); - }); - - describe('event', () => { - it('should handle click events', ({ container }) => { - const handleClick = vi.fn(); - - function App() { - return ; - } - - render(App, container); - const button = container.querySelector('button'); - button.click(); - - expect(handleClick).toHaveBeenCalled(); - }); - - it('should support inline event update', ({ container }) => { - function App() { - let count = 0; - return ; - } - - render(App, container); - expect(container.innerHTML).toEqual(''); - const button = container.querySelector('button'); - button.click(); - expect(container.innerHTML).toEqual(''); - }); - - it('should support inline event for component props', ({ container }) => { - function Button({ onClick, children }) { - return ; - } - - function App() { - let value = ''; - - return ; - } - - render(App, container); - expect(container.innerHTML).toEqual(''); - const button = container.querySelector('button')!; - button.click(); - expect(container.innerHTML).toEqual(''); - }); - }); - - describe('components', () => { - it('should render components', ({ container }) => { - function Button({ children }) { - return ; - } - - function App() { - return ( -
    -

    hello world!!!

    - -
    - ); - } - - render(App, container); - expect(container).toMatchInlineSnapshot(` -
    -
    -

    - hello world!!! -

    - -
    -
    - `); - }); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render, View } from '../src'; + +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); + +describe('rendering', () => { + describe('basic', () => { + it('should support basic dom', ({ container }) => { + function App() { + return

    hello world!!!

    ; + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +

    + hello world!!! +

    +
    + `); + }); + + it('should support text and variable mixing', ({ container }) => { + function App() { + const name = 'world'; + return

    hello {name}!!!

    ; + } + + render(App, container); + expect(container.innerHTML).toBe('

    hello world!!!

    '); + }); + + // TODO: SHOULD FIX + it('should support dom has multiple layers ', ({ container }) => { + function App() { + let count = 0; + return ( +
    + Header +

    hello world!!!

    +
    + +
    + Footer +
    + ); + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
    + Header +

    + hello world!!! +

    +
    + +
    + Footer +
    +
    + `); + }); + + // TODO: SHOULD FIX + it('should support tag, text and variable mixing', ({ container }) => { + function App() { + let count = 'world'; + + return ( +
    + count:{count} + +
    + ); + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
    + count: + world + +
    +
    + `); + }); + }); + + describe('style', () => { + it('should apply styles correctly', ({ container }) => { + function App() { + return

    hello world!!!

    ; + } + + render(App, container); + const h1 = container.querySelector('h1'); + expect(h1.style.color).toBe('red'); + }); + + it.fails('should apply styles number correctly', ({ container }) => { + function App() { + return

    hello world!!!

    ; + } + + render(App, container); + const h1 = container.querySelector('h1'); + expect(h1.style.fontSize).toBe('12px'); + }); + + it('should apply styles empty correctly', ({ container }) => { + function App() { + return

    hello world!!!

    ; + } + + render(App, container); + const h1 = container.querySelector('h1'); + expect(h1.style.fontSize).toBe(''); + }); + + it('should apply multiple styles correctly', ({ container }) => { + function App() { + return

    hello world!!!

    ; + } + + render(App, container); + const h1 = container.querySelector('h1'); + expect(h1.style.color).toBe('red'); + expect(h1.style.fontSize).toBe('20px'); + }); + + it('should override styles correctly', ({ container }) => { + function App() { + return ( +

    + hello world!!! +

    + ); + } + + render(App, container); + const span = container.querySelector('span'); + expect(span.style.color).toBe('blue'); + }); + + it('should handle dynamic styles', ({ container }) => { + const color = 'red'; + + function App() { + return

    hello world!!!

    ; + } + + render(App, container); + const h1 = container.querySelector('h1'); + expect(h1.style.color).toBe('red'); + }); + + it('should handle style object', ({ container }) => { + const color = 'red'; + + function App() { + const style = { color }; + return

    hello world!!!

    ; + } + + render(App, container); + const h1 = container.querySelector('h1'); + expect(h1.style.color).toBe('red'); + }); + + it('should update style', ({ container }) => { + function App() { + let color = 'red'; + return ( +

    (color = 'blue')}> + hello world!!! +

    + ); + } + + render(App, container); + const h1 = container.querySelector('h1'); + expect(h1.style.color).toBe('red'); + h1.click(); + expect(h1.style.color).toBe('blue'); + }); + + it('should update when object style changed', ({ container }) => { + function App() { + let style = { color: 'red', fontSize: '20px' }; + return ( +
    { + style = { color: 'blue', height: '20px' }; + }} + > + hello world!!! +
    + ); + } + + render(App, container); + const div = container.querySelector('div'); + expect(div.style.color).toBe('red'); + expect(div.style.fontSize).toBe('20px'); + expect(div.style.height).toBe(''); + div.click(); + expect(div.style.color).toBe('blue'); + expect(div.style.height).toBe('20px'); + expect(div.style.fontSize).toBe(''); + }); + }); + + describe('event', () => { + it('should handle click events', ({ container }) => { + const handleClick = vi.fn(); + + function App() { + return ; + } + + render(App, container); + const button = container.querySelector('button'); + button.click(); + + expect(handleClick).toHaveBeenCalled(); + }); + + it('should support inline event update', ({ container }) => { + function App() { + let count = 0; + return ; + } + + render(App, container); + expect(container.innerHTML).toEqual(''); + const button = container.querySelector('button'); + button.click(); + expect(container.innerHTML).toEqual(''); + }); + + it('should support inline event for component props', ({ container }) => { + function Button({ onClick, children }) { + return ; + } + + function App() { + let value = ''; + + return ; + } + + render(App, container); + expect(container.innerHTML).toEqual(''); + const button = container.querySelector('button')!; + button.click(); + expect(container.innerHTML).toEqual(''); + }); + }); + + describe('components', () => { + it('should render components', ({ container }) => { + function Button({ children }) { + return ; + } + + function App() { + return ( +
    +

    hello world!!!

    + +
    + ); + } + + render(App, container); + expect(container).toMatchInlineSnapshot(` +
    +
    +

    + hello world!!! +

    + +
    +
    + `); + }); + }); +}); diff --git a/packages/inula-next/test/state.test.tsx b/packages/inula-next/test/state.test.tsx index d5156f38281cd97f66bcf796d3baaf8ab38d0824..c5deeec59c72e90dd57a92693783752120ddc1fe 100644 --- a/packages/inula-next/test/state.test.tsx +++ b/packages/inula-next/test/state.test.tsx @@ -1,375 +1,375 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render, View } from '../src'; - -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); - -describe('Declare state', () => { - it('Should correctly declare and render a string state variable', ({ container }) => { - function StringState() { - let str = 'Hello, World!'; - return

    {str}

    ; - } - render(StringState, container); - expect(container.innerHTML).toBe('

    Hello, World!

    '); - }); - - it('Should correctly declare and render a number state variable', ({ container }) => { - function NumberState() { - let num = 42; - return

    {num}

    ; - } - render(NumberState, container); - expect(container.innerHTML).toBe('

    42

    '); - }); - - it('Should correctly declare and render a boolean state variable', ({ container }) => { - function BooleanState() { - let bool = true; - return

    {bool.toString()}

    ; - } - render(BooleanState, container); - expect(container.innerHTML).toBe('

    true

    '); - }); - - it('Should correctly declare and render an array state variable', ({ container }) => { - function ArrayState() { - let arr = [1, 2, 3]; - return

    {arr.join(', ')}

    ; - } - render(ArrayState, container); - expect(container.innerHTML).toBe('

    1, 2, 3

    '); - }); - - it('Should correctly declare and render an object state variable', ({ container }) => { - function ObjectState() { - let obj = { name: 'John', age: 30 }; - return

    {`${obj.name}, ${obj.age}`}

    ; - } - render(ObjectState, container); - expect(container.innerHTML).toBe('

    John, 30

    '); - }); - - it('Should correctly declare and render a map state variable', ({ container }) => { - function MapState() { - let map = new Map([ - ['key1', 'value1'], - ['key2', 'value2'], - ]); - return ( -

    - {Array.from(map) - .map(([k, v]) => `${k}:${v}`) - .join(', ')} -

    - ); - } - render(MapState, container); - expect(container.innerHTML).toBe('

    key1:value1, key2:value2

    '); - }); - - it('Should correctly declare and render a set state variable', ({ container }) => { - function SetState() { - let set = new Set([1, 2, 3]); - return

    {Array.from(set).join(', ')}

    ; - } - render(SetState, container); - expect(container.innerHTML).toBe('

    1, 2, 3

    '); - }); -}); - -describe('Update state', () => { - it('Should correctly update and render a string state variable', ({ container }) => { - let updateStr: () => void; - function StringState() { - let str = 'Hello'; - updateStr = () => { - str = 'Hello, World!'; - }; - return

    {str}

    ; - } - render(StringState, container); - expect(container.innerHTML).toBe('

    Hello

    '); - updateStr(); - expect(container.innerHTML).toBe('

    Hello, World!

    '); - }); - - it('Should correctly update and render a number state variable', ({ container }) => { - let updateNum: () => void; - function NumberState() { - let num = 42; - updateNum = () => { - num = 84; - }; - return

    {num}

    ; - } - render(NumberState, container); - expect(container.innerHTML).toBe('

    42

    '); - updateNum(); - expect(container.innerHTML).toBe('

    84

    '); - }); - - it('Should correctly update and render a boolean state variable', ({ container }) => { - let toggleBool: () => void; - function BooleanState() { - let bool = true; - toggleBool = () => { - bool = !bool; - }; - return

    {bool.toString()}

    ; - } - render(BooleanState, container); - expect(container.innerHTML).toBe('

    true

    '); - toggleBool(); - expect(container.innerHTML).toBe('

    false

    '); - }); - - it('Should correctly update and render an object state variable', ({ container }) => { - let updateObj: () => void; - function ObjectState() { - let obj = { name: 'John', age: 30 }; - updateObj = () => { - obj.age = 31; - }; - return

    {`${obj.name}, ${obj.age}`}

    ; - } - render(ObjectState, container); - expect(container.innerHTML).toBe('

    John, 30

    '); - updateObj(); - expect(container.innerHTML).toBe('

    John, 31

    '); - }); - - it('Should correctly handle increment operations (n++)', ({ container }) => { - let increment: () => void; - function IncrementState() { - let count = 0; - increment = () => { - count++; - }; - return

    {count}

    ; - } - render(IncrementState, container); - expect(container.innerHTML).toBe('

    0

    '); - increment(); - expect(container.innerHTML).toBe('

    1

    '); - }); - - it('Should correctly handle decrement operations (n--)', ({ container }) => { - let decrement: () => void; - function DecrementState() { - let count = 5; - decrement = () => { - count--; - }; - return

    {count}

    ; - } - render(DecrementState, container); - expect(container.innerHTML).toBe('

    5

    '); - decrement(); - expect(container.innerHTML).toBe('

    4

    '); - }); - - it('Should correctly handle operations (+=)', ({ container }) => { - let addValue: (value: number) => void; - function AdditionAssignmentState() { - let count = 10; - addValue = (value: number) => { - count += value; - }; - return

    {count}

    ; - } - render(AdditionAssignmentState, container); - expect(container.innerHTML).toBe('

    10

    '); - addValue(5); - expect(container.innerHTML).toBe('

    15

    '); - addValue(-3); - expect(container.innerHTML).toBe('

    12

    '); - }); - - it('Should correctly handle operations (-=)', ({ container }) => { - let subtractValue: (value: number) => void; - function SubtractionAssignmentState() { - let count = 20; - subtractValue = (value: number) => { - count -= value; - }; - return

    {count}

    ; - } - render(SubtractionAssignmentState, container); - expect(container.innerHTML).toBe('

    20

    '); - subtractValue(7); - expect(container.innerHTML).toBe('

    13

    '); - subtractValue(-4); - expect(container.innerHTML).toBe('

    17

    '); - }); - - it('Should correctly update and render a state variable as an index of array', ({ container }) => { - let updateIndex: () => void; - function ArrayIndexState() { - const items = ['Apple', 'Banana', 'Cherry', 'Date']; - let index = 0; - updateIndex = () => { - index = (index + 1) % items.length; - }; - return

    {items[index]}

    ; - } - render(ArrayIndexState, container); - expect(container.innerHTML).toBe('

    Apple

    '); - updateIndex(); - expect(container.innerHTML).toBe('

    Banana

    '); - updateIndex(); - expect(container.innerHTML).toBe('

    Cherry

    '); - updateIndex(); - expect(container.innerHTML).toBe('

    Date

    '); - updateIndex(); - expect(container.innerHTML).toBe('

    Apple

    '); - }); - - it('Should correctly update and render a state variable as a property of an object', ({ container }) => { - let updatePerson: () => void; - function ObjectPropertyState() { - let person = { name: 'Alice', age: 30, job: 'Engineer' }; - updatePerson = () => { - person.age += 1; - person.job = 'Senior Engineer'; - }; - return ( -
    -

    Name: {person.name}

    -

    Age: {person.age}

    -

    Job: {person.job}

    -
    - ); - } - render(ObjectPropertyState, container); - expect(container.innerHTML).toBe('

    Name: Alice

    Age: 30

    Job: Engineer

    '); - updatePerson(); - expect(container.innerHTML).toBe('

    Name: Alice

    Age: 31

    Job: Senior Engineer

    '); - }); - - it('Should correctly update and render an array state variable - push operation', ({ container }) => { - let pushItem: () => void; - function ArrayPushState() { - let items = ['Apple', 'Banana']; - pushItem = () => { - items.push('Cherry'); - }; - return

    {items.join(', ')}

    ; - } - render(ArrayPushState, container); - expect(container.innerHTML).toBe('

    Apple, Banana

    '); - pushItem(); - expect(container.innerHTML).toBe('

    Apple, Banana, Cherry

    '); - }); - - it('Should correctly update and render an array state variable - pop operation', ({ container }) => { - let popItem: () => void; - function ArrayPopState() { - let items = ['Apple', 'Banana', 'Cherry']; - popItem = () => { - items.pop(); - }; - return

    {items.join(', ')}

    ; - } - render(ArrayPopState, container); - expect(container.innerHTML).toBe('

    Apple, Banana, Cherry

    '); - popItem(); - expect(container.innerHTML).toBe('

    Apple, Banana

    '); - }); - - it('Should correctly update and render an array state variable - unshift operation', ({ container }) => { - let unshiftItem: () => void; - function ArrayUnshiftState() { - let items = ['Banana', 'Cherry']; - unshiftItem = () => { - items.unshift('Apple'); - }; - return

    {items.join(', ')}

    ; - } - render(ArrayUnshiftState, container); - expect(container.innerHTML).toBe('

    Banana, Cherry

    '); - unshiftItem(); - expect(container.innerHTML).toBe('

    Apple, Banana, Cherry

    '); - }); - - it('Should correctly update and render an array state variable - shift operation', ({ container }) => { - let shiftItem: () => void; - function ArrayShiftState() { - let items = ['Apple', 'Banana', 'Cherry']; - shiftItem = () => { - items.shift(); - }; - return

    {items.join(', ')}

    ; - } - render(ArrayShiftState, container); - expect(container.innerHTML).toBe('

    Apple, Banana, Cherry

    '); - shiftItem(); - expect(container.innerHTML).toBe('

    Banana, Cherry

    '); - }); - - it('Should correctly update and render an array state variable - splice operation', ({ container }) => { - let spliceItems: () => void; - function ArraySpliceState() { - let items = ['Apple', 'Banana', 'Cherry', 'Date']; - spliceItems = () => { - items.splice(1, 2, 'Elderberry', 'Fig'); - }; - return

    {items.join(', ')}

    ; - } - render(ArraySpliceState, container); - expect(container.innerHTML).toBe('

    Apple, Banana, Cherry, Date

    '); - spliceItems(); - expect(container.innerHTML).toBe('

    Apple, Elderberry, Fig, Date

    '); - }); - - it('Should correctly update and render an array state variable - filter operation', ({ container }) => { - let filterItems: () => void; - function ArrayFilterState() { - let items = ['Apple', 'Banana', 'Cherry', 'Date']; - filterItems = () => { - items = items.filter(item => item.length > 5); - }; - return

    {items.join(', ')}

    ; - } - render(ArrayFilterState, container); - expect(container.innerHTML).toBe('

    Apple, Banana, Cherry, Date

    '); - filterItems(); - expect(container.innerHTML).toBe('

    Banana, Cherry

    '); - }); - - it('Should correctly update and render an array state variable - map operation', ({ container }) => { - let mapItems: () => void; - function ArrayMapState() { - let items = ['apple', 'banana', 'cherry']; - mapItems = () => { - items = items.map(item => item.toUpperCase()); - }; - return

    {items.join(', ')}

    ; - } - render(ArrayMapState, container); - expect(container.innerHTML).toBe('

    apple, banana, cherry

    '); - mapItems(); - expect(container.innerHTML).toBe('

    APPLE, BANANA, CHERRY

    '); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render, View } from '../src'; + +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); + +describe('Declare state', () => { + it('Should correctly declare and render a string state variable', ({ container }) => { + function StringState() { + let str = 'Hello, World!'; + return

    {str}

    ; + } + render(StringState, container); + expect(container.innerHTML).toBe('

    Hello, World!

    '); + }); + + it('Should correctly declare and render a number state variable', ({ container }) => { + function NumberState() { + let num = 42; + return

    {num}

    ; + } + render(NumberState, container); + expect(container.innerHTML).toBe('

    42

    '); + }); + + it('Should correctly declare and render a boolean state variable', ({ container }) => { + function BooleanState() { + let bool = true; + return

    {bool.toString()}

    ; + } + render(BooleanState, container); + expect(container.innerHTML).toBe('

    true

    '); + }); + + it('Should correctly declare and render an array state variable', ({ container }) => { + function ArrayState() { + let arr = [1, 2, 3]; + return

    {arr.join(', ')}

    ; + } + render(ArrayState, container); + expect(container.innerHTML).toBe('

    1, 2, 3

    '); + }); + + it('Should correctly declare and render an object state variable', ({ container }) => { + function ObjectState() { + let obj = { name: 'John', age: 30 }; + return

    {`${obj.name}, ${obj.age}`}

    ; + } + render(ObjectState, container); + expect(container.innerHTML).toBe('

    John, 30

    '); + }); + + it('Should correctly declare and render a map state variable', ({ container }) => { + function MapState() { + let map = new Map([ + ['key1', 'value1'], + ['key2', 'value2'], + ]); + return ( +

    + {Array.from(map) + .map(([k, v]) => `${k}:${v}`) + .join(', ')} +

    + ); + } + render(MapState, container); + expect(container.innerHTML).toBe('

    key1:value1, key2:value2

    '); + }); + + it('Should correctly declare and render a set state variable', ({ container }) => { + function SetState() { + let set = new Set([1, 2, 3]); + return

    {Array.from(set).join(', ')}

    ; + } + render(SetState, container); + expect(container.innerHTML).toBe('

    1, 2, 3

    '); + }); +}); + +describe('Update state', () => { + it('Should correctly update and render a string state variable', ({ container }) => { + let updateStr: () => void; + function StringState() { + let str = 'Hello'; + updateStr = () => { + str = 'Hello, World!'; + }; + return

    {str}

    ; + } + render(StringState, container); + expect(container.innerHTML).toBe('

    Hello

    '); + updateStr(); + expect(container.innerHTML).toBe('

    Hello, World!

    '); + }); + + it('Should correctly update and render a number state variable', ({ container }) => { + let updateNum: () => void; + function NumberState() { + let num = 42; + updateNum = () => { + num = 84; + }; + return

    {num}

    ; + } + render(NumberState, container); + expect(container.innerHTML).toBe('

    42

    '); + updateNum(); + expect(container.innerHTML).toBe('

    84

    '); + }); + + it('Should correctly update and render a boolean state variable', ({ container }) => { + let toggleBool: () => void; + function BooleanState() { + let bool = true; + toggleBool = () => { + bool = !bool; + }; + return

    {bool.toString()}

    ; + } + render(BooleanState, container); + expect(container.innerHTML).toBe('

    true

    '); + toggleBool(); + expect(container.innerHTML).toBe('

    false

    '); + }); + + it('Should correctly update and render an object state variable', ({ container }) => { + let updateObj: () => void; + function ObjectState() { + let obj = { name: 'John', age: 30 }; + updateObj = () => { + obj.age = 31; + }; + return

    {`${obj.name}, ${obj.age}`}

    ; + } + render(ObjectState, container); + expect(container.innerHTML).toBe('

    John, 30

    '); + updateObj(); + expect(container.innerHTML).toBe('

    John, 31

    '); + }); + + it('Should correctly handle increment operations (n++)', ({ container }) => { + let increment: () => void; + function IncrementState() { + let count = 0; + increment = () => { + count++; + }; + return

    {count}

    ; + } + render(IncrementState, container); + expect(container.innerHTML).toBe('

    0

    '); + increment(); + expect(container.innerHTML).toBe('

    1

    '); + }); + + it('Should correctly handle decrement operations (n--)', ({ container }) => { + let decrement: () => void; + function DecrementState() { + let count = 5; + decrement = () => { + count--; + }; + return

    {count}

    ; + } + render(DecrementState, container); + expect(container.innerHTML).toBe('

    5

    '); + decrement(); + expect(container.innerHTML).toBe('

    4

    '); + }); + + it('Should correctly handle operations (+=)', ({ container }) => { + let addValue: (value: number) => void; + function AdditionAssignmentState() { + let count = 10; + addValue = (value: number) => { + count += value; + }; + return

    {count}

    ; + } + render(AdditionAssignmentState, container); + expect(container.innerHTML).toBe('

    10

    '); + addValue(5); + expect(container.innerHTML).toBe('

    15

    '); + addValue(-3); + expect(container.innerHTML).toBe('

    12

    '); + }); + + it('Should correctly handle operations (-=)', ({ container }) => { + let subtractValue: (value: number) => void; + function SubtractionAssignmentState() { + let count = 20; + subtractValue = (value: number) => { + count -= value; + }; + return

    {count}

    ; + } + render(SubtractionAssignmentState, container); + expect(container.innerHTML).toBe('

    20

    '); + subtractValue(7); + expect(container.innerHTML).toBe('

    13

    '); + subtractValue(-4); + expect(container.innerHTML).toBe('

    17

    '); + }); + + it('Should correctly update and render a state variable as an index of array', ({ container }) => { + let updateIndex: () => void; + function ArrayIndexState() { + const items = ['Apple', 'Banana', 'Cherry', 'Date']; + let index = 0; + updateIndex = () => { + index = (index + 1) % items.length; + }; + return

    {items[index]}

    ; + } + render(ArrayIndexState, container); + expect(container.innerHTML).toBe('

    Apple

    '); + updateIndex(); + expect(container.innerHTML).toBe('

    Banana

    '); + updateIndex(); + expect(container.innerHTML).toBe('

    Cherry

    '); + updateIndex(); + expect(container.innerHTML).toBe('

    Date

    '); + updateIndex(); + expect(container.innerHTML).toBe('

    Apple

    '); + }); + + it('Should correctly update and render a state variable as a property of an object', ({ container }) => { + let updatePerson: () => void; + function ObjectPropertyState() { + let person = { name: 'Alice', age: 30, job: 'Engineer' }; + updatePerson = () => { + person.age += 1; + person.job = 'Senior Engineer'; + }; + return ( +
    +

    Name: {person.name}

    +

    Age: {person.age}

    +

    Job: {person.job}

    +
    + ); + } + render(ObjectPropertyState, container); + expect(container.innerHTML).toBe('

    Name: Alice

    Age: 30

    Job: Engineer

    '); + updatePerson(); + expect(container.innerHTML).toBe('

    Name: Alice

    Age: 31

    Job: Senior Engineer

    '); + }); + + it('Should correctly update and render an array state variable - push operation', ({ container }) => { + let pushItem: () => void; + function ArrayPushState() { + let items = ['Apple', 'Banana']; + pushItem = () => { + items.push('Cherry'); + }; + return

    {items.join(', ')}

    ; + } + render(ArrayPushState, container); + expect(container.innerHTML).toBe('

    Apple, Banana

    '); + pushItem(); + expect(container.innerHTML).toBe('

    Apple, Banana, Cherry

    '); + }); + + it('Should correctly update and render an array state variable - pop operation', ({ container }) => { + let popItem: () => void; + function ArrayPopState() { + let items = ['Apple', 'Banana', 'Cherry']; + popItem = () => { + items.pop(); + }; + return

    {items.join(', ')}

    ; + } + render(ArrayPopState, container); + expect(container.innerHTML).toBe('

    Apple, Banana, Cherry

    '); + popItem(); + expect(container.innerHTML).toBe('

    Apple, Banana

    '); + }); + + it('Should correctly update and render an array state variable - unshift operation', ({ container }) => { + let unshiftItem: () => void; + function ArrayUnshiftState() { + let items = ['Banana', 'Cherry']; + unshiftItem = () => { + items.unshift('Apple'); + }; + return

    {items.join(', ')}

    ; + } + render(ArrayUnshiftState, container); + expect(container.innerHTML).toBe('

    Banana, Cherry

    '); + unshiftItem(); + expect(container.innerHTML).toBe('

    Apple, Banana, Cherry

    '); + }); + + it('Should correctly update and render an array state variable - shift operation', ({ container }) => { + let shiftItem: () => void; + function ArrayShiftState() { + let items = ['Apple', 'Banana', 'Cherry']; + shiftItem = () => { + items.shift(); + }; + return

    {items.join(', ')}

    ; + } + render(ArrayShiftState, container); + expect(container.innerHTML).toBe('

    Apple, Banana, Cherry

    '); + shiftItem(); + expect(container.innerHTML).toBe('

    Banana, Cherry

    '); + }); + + it('Should correctly update and render an array state variable - splice operation', ({ container }) => { + let spliceItems: () => void; + function ArraySpliceState() { + let items = ['Apple', 'Banana', 'Cherry', 'Date']; + spliceItems = () => { + items.splice(1, 2, 'Elderberry', 'Fig'); + }; + return

    {items.join(', ')}

    ; + } + render(ArraySpliceState, container); + expect(container.innerHTML).toBe('

    Apple, Banana, Cherry, Date

    '); + spliceItems(); + expect(container.innerHTML).toBe('

    Apple, Elderberry, Fig, Date

    '); + }); + + it('Should correctly update and render an array state variable - filter operation', ({ container }) => { + let filterItems: () => void; + function ArrayFilterState() { + let items = ['Apple', 'Banana', 'Cherry', 'Date']; + filterItems = () => { + items = items.filter(item => item.length > 5); + }; + return

    {items.join(', ')}

    ; + } + render(ArrayFilterState, container); + expect(container.innerHTML).toBe('

    Apple, Banana, Cherry, Date

    '); + filterItems(); + expect(container.innerHTML).toBe('

    Banana, Cherry

    '); + }); + + it('Should correctly update and render an array state variable - map operation', ({ container }) => { + let mapItems: () => void; + function ArrayMapState() { + let items = ['apple', 'banana', 'cherry']; + mapItems = () => { + items = items.map(item => item.toUpperCase()); + }; + return

    {items.join(', ')}

    ; + } + render(ArrayMapState, container); + expect(container.innerHTML).toBe('

    apple, banana, cherry

    '); + mapItems(); + expect(container.innerHTML).toBe('

    APPLE, BANANA, CHERRY

    '); + }); +}); diff --git a/packages/inula-next/test/utils.ts b/packages/inula-next/test/utils.ts index b93b745bfe85f00db1b51fa5297518c3bf811e21..f30613d553d1ee60b82cc7afecbbfc8eb3e0c016 100644 --- a/packages/inula-next/test/utils.ts +++ b/packages/inula-next/test/utils.ts @@ -1,30 +1,30 @@ -/* - * 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 { test } from 'vitest'; - -interface DomTestContext { - container: HTMLDivElement; -} - -// Define a new test type that extends the default test type and adds the container fixture. -export const domTest = test.extend({ - container: async ({ task }, use) => { - const container = document.createElement('div'); - document.body.appendChild(container); - await use(container); - container.remove(); - }, -}); +/* + * 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 { test } from 'vitest'; + +interface DomTestContext { + container: HTMLDivElement; +} + +// Define a new test type that extends the default test type and adds the container fixture. +export const domTest = test.extend({ + container: async ({ task }, use) => { + const container = document.createElement('div'); + document.body.appendChild(container); + await use(container); + container.remove(); + }, +}); diff --git a/packages/inula-next/test/watch.test.tsx b/packages/inula-next/test/watch.test.tsx index 0a87a38ea72bb83f429ea30673d3d070ba1b03c5..5ad2518ba6c59874acf7d752b7711b2c31b9fb22 100644 --- a/packages/inula-next/test/watch.test.tsx +++ b/packages/inula-next/test/watch.test.tsx @@ -1,325 +1,325 @@ -/* - * 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, vi } from 'vitest'; -import { domTest as it } from './utils'; -import { render, View } from '../src'; - -vi.mock('../src/scheduler', async () => { - return { - schedule: (task: () => void) => { - task(); - }, - }; -}); - -describe('Watch', () => { - it('Should correctly watch variable', ({ container }) => { - let increment: () => void; - function TrafficLight() { - let lightIndex = 0; - let index = 0; - increment = () => { - lightIndex = lightIndex + 1; - }; - - watch(() => { - index = lightIndex; - }); - return
    {index}
    ; - } - render(TrafficLight, container); - expect(container.innerHTML).toBe('
    0
    '); - increment(); - expect(container.innerHTML).toBe('
    1
    '); - }); - it('Should correctly watch and update based on string state changes', ({ container }) => { - let updateState: (newState: string) => void; - function StringComponent() { - let state = 'initial'; - let watchedState = ''; - updateState = (newState: string) => { - state = newState; - }; - watch(() => { - watchedState = state; - }); - return
    {watchedState}
    ; - } - render(StringComponent, container); - expect(container.innerHTML).toBe('
    initial
    '); - updateState('updated'); - expect(container.innerHTML).toBe('
    updated
    '); - }); - - it('Should correctly watch and update based on number state changes', ({ container }) => { - let updateState: (newState: number) => void; - function NumberComponent() { - let state = 0; - let watchedState = 0; - updateState = (newState: number) => { - state = newState; - }; - watch(() => { - watchedState = state; - }); - return
    {watchedState}
    ; - } - render(NumberComponent, container); - expect(container.innerHTML).toBe('
    0
    '); - updateState(42); - expect(container.innerHTML).toBe('
    42
    '); - }); - - it('Should correctly watch and update based on boolean state changes', ({ container }) => { - let updateState: (newState: boolean) => void; - function BooleanComponent() { - let state = false; - let watchedState = false; - updateState = (newState: boolean) => { - state = newState; - }; - watch(() => { - watchedState = state; - }); - return
    {watchedState.toString()}
    ; - } - render(BooleanComponent, container); - expect(container.innerHTML).toBe('
    false
    '); - updateState(true); - expect(container.innerHTML).toBe('
    true
    '); - }); - - it('Should correctly watch and update based on array state changes', ({ container }) => { - let updateState: (newState: number[]) => void; - function ArrayComponent() { - let state: number[] = []; - let watchedState: number[] = []; - updateState = (newState: number[]) => { - state = newState; - }; - watch(() => { - watchedState = [...state]; - }); - return
    {watchedState.join(',')}
    ; - } - render(ArrayComponent, container); - expect(container.innerHTML).toBe('
    '); - updateState([1, 2, 3]); - expect(container.innerHTML).toBe('
    1,2,3
    '); - }); - - it('Should correctly watch and update based on object state changes', ({ container }) => { - let updateState: (newState: { [key: string]: any }) => void; - function ObjectComponent() { - let state = {}; - let watchedState = {}; - updateState = (newState: { [key: string]: any }) => { - state = newState; - }; - watch(() => { - watchedState = { ...state }; - }); - return
    {JSON.stringify(watchedState)}
    ; - } - render(ObjectComponent, container); - expect(container.innerHTML).toBe('
    {}
    '); - updateState({ key: 'value' }); - expect(container.innerHTML).toBe('
    {"key":"value"}
    '); - }); - - it('Should correctly watch conditional expressions', ({ container }) => { - let updateX: (value: number) => void; - function ConditionalComponent() { - let x = 5; - let result = ''; - updateX = (value: number) => { - x = value; - }; - watch(() => { - result = x > 10 ? 'Greater than 10' : 'Less than or equal to 10'; - }); - return
    {result}
    ; - } - render(ConditionalComponent, container); - expect(container.innerHTML).toBe('
    Less than or equal to 10
    '); - updateX(15); - expect(container.innerHTML).toBe('
    Greater than 10
    '); - }); - - it('Should correctly watch template strings', ({ container }) => { - let updateName: (value: string) => void; - let updateAge: (value: number) => void; - function TemplateStringComponent() { - let name = 'John'; - let age = 30; - let message = ''; - updateName = (value: string) => { - name = value; - }; - updateAge = (value: number) => { - age = value; - }; - watch(() => { - message = `${name} is ${age} years old`; - }); - return
    {message}
    ; - } - render(TemplateStringComponent, container); - expect(container.innerHTML).toBe('
    John is 30 years old
    '); - updateName('Jane'); - updateAge(25); - expect(container.innerHTML).toBe('
    Jane is 25 years old
    '); - }); - - it('Should correctly watch arithmetic operations', ({ container }) => { - let updateX: (value: number) => void; - let updateY: (value: number) => void; - function ArithmeticComponent() { - let x = 5; - let y = 3; - let result = 0; - updateX = (value: number) => { - x = value; - }; - updateY = (value: number) => { - y = value; - }; - watch(() => { - result = x + y * 2 - x / y; - }); - return
    {result}
    ; - } - render(ArithmeticComponent, container); - expect(container.innerHTML).toBe('
    9.333333333333334
    '); - updateX(10); - updateY(5); - expect(container.innerHTML).toBe('
    18
    '); - }); - - it('Should correctly watch property access and indexing', ({ container }) => { - let updateObj: (value: { [key: string]: any }) => void; - let updateArr: (value: number[]) => void; - function AccessComponent() { - let obj = { a: 1, b: 2 }; - let arr = [1, 2, 3]; - let result = 0; - updateObj = (value: { [key: string]: any }) => { - obj = value; - }; - updateArr = (value: number[]) => { - arr = value; - }; - watch(() => { - result = obj.a + arr[1]; - }); - return
    {result}
    ; - } - render(AccessComponent, container); - expect(container.innerHTML).toBe('
    3
    '); - updateObj({ a: 5, b: 6 }); - updateArr([0, 10, 20]); - expect(container.innerHTML).toBe('
    15
    '); - }); - - it('Should correctly watch function calls', ({ container }) => { - let updateX: (value: number) => void; - function FunctionCallComponent() { - let x = 5; - let result = 0; - const square = (n: number) => n * n; - updateX = (value: number) => { - x = value; - }; - watch(() => { - result = square(x); - }); - return
    {result}
    ; - } - render(FunctionCallComponent, container); - expect(container.innerHTML).toBe('
    25
    '); - updateX(10); - expect(container.innerHTML).toBe('
    100
    '); - }); - - it('Should correctly watch various number operations', ({ container }) => { - let updateX: (value: number) => void; - function NumberOpsComponent() { - let x = 16; - let result = ''; - updateX = (value: number) => { - x = value; - }; - watch(() => { - result = `sqrt: ${Math.sqrt(x)}, floor: ${Math.floor(x / 3)}, pow: ${Math.pow(x, 2)}`; - }); - return
    {result}
    ; - } - render(NumberOpsComponent, container); - expect(container.innerHTML).toBe('
    sqrt: 4, floor: 5, pow: 256
    '); - updateX(25); - expect(container.innerHTML).toBe('
    sqrt: 5, floor: 8, pow: 625
    '); - }); - - it('Should correctly watch map operations', ({ container }) => { - let updateArr: (value: number[]) => void; - function MapComponent() { - let arr = [1, 2, 3, 4, 5]; - let result: number[] = []; - updateArr = (value: number[]) => { - arr = value; - }; - watch(() => { - result = arr.map(x => x * 2); - }); - return
    {result.join(', ')}
    ; - } - render(MapComponent, container); - expect(container.innerHTML).toBe('
    2, 4, 6, 8, 10
    '); - updateArr([10, 20, 30]); - expect(container.innerHTML).toBe('
    20, 40, 60
    '); - }); - - it('Should correctly watch multiple variables', ({ container }) => { - let updateX: (value: number) => void; - let updateY: (value: string) => void; - let updateZ: (value: boolean) => void; - function MultiWatchComponent() { - let x = 5; - let y = 'hello'; - let z = true; - let result = ''; - updateX = (value: number) => { - x = value; - }; - updateY = (value: string) => { - y = value; - }; - updateZ = (value: boolean) => { - z = value; - }; - watch(() => { - result = `x: ${x}, y: ${y}, z: ${z}`; - }); - return
    {result}
    ; - } - render(MultiWatchComponent, container); - expect(container.innerHTML).toBe('
    x: 5, y: hello, z: true
    '); - updateX(10); - updateY('world'); - updateZ(false); - expect(container.innerHTML).toBe('
    x: 10, y: world, z: false
    '); - }); -}); +/* + * 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, vi } from 'vitest'; +import { domTest as it } from './utils'; +import { render, View } from '../src'; + +vi.mock('../src/scheduler', async () => { + return { + schedule: (task: () => void) => { + task(); + }, + }; +}); + +describe('Watch', () => { + it('Should correctly watch variable', ({ container }) => { + let increment: () => void; + function TrafficLight() { + let lightIndex = 0; + let index = 0; + increment = () => { + lightIndex = lightIndex + 1; + }; + + watch(() => { + index = lightIndex; + }); + return
    {index}
    ; + } + render(TrafficLight, container); + expect(container.innerHTML).toBe('
    0
    '); + increment(); + expect(container.innerHTML).toBe('
    1
    '); + }); + it('Should correctly watch and update based on string state changes', ({ container }) => { + let updateState: (newState: string) => void; + function StringComponent() { + let state = 'initial'; + let watchedState = ''; + updateState = (newState: string) => { + state = newState; + }; + watch(() => { + watchedState = state; + }); + return
    {watchedState}
    ; + } + render(StringComponent, container); + expect(container.innerHTML).toBe('
    initial
    '); + updateState('updated'); + expect(container.innerHTML).toBe('
    updated
    '); + }); + + it('Should correctly watch and update based on number state changes', ({ container }) => { + let updateState: (newState: number) => void; + function NumberComponent() { + let state = 0; + let watchedState = 0; + updateState = (newState: number) => { + state = newState; + }; + watch(() => { + watchedState = state; + }); + return
    {watchedState}
    ; + } + render(NumberComponent, container); + expect(container.innerHTML).toBe('
    0
    '); + updateState(42); + expect(container.innerHTML).toBe('
    42
    '); + }); + + it('Should correctly watch and update based on boolean state changes', ({ container }) => { + let updateState: (newState: boolean) => void; + function BooleanComponent() { + let state = false; + let watchedState = false; + updateState = (newState: boolean) => { + state = newState; + }; + watch(() => { + watchedState = state; + }); + return
    {watchedState.toString()}
    ; + } + render(BooleanComponent, container); + expect(container.innerHTML).toBe('
    false
    '); + updateState(true); + expect(container.innerHTML).toBe('
    true
    '); + }); + + it('Should correctly watch and update based on array state changes', ({ container }) => { + let updateState: (newState: number[]) => void; + function ArrayComponent() { + let state: number[] = []; + let watchedState: number[] = []; + updateState = (newState: number[]) => { + state = newState; + }; + watch(() => { + watchedState = [...state]; + }); + return
    {watchedState.join(',')}
    ; + } + render(ArrayComponent, container); + expect(container.innerHTML).toBe('
    '); + updateState([1, 2, 3]); + expect(container.innerHTML).toBe('
    1,2,3
    '); + }); + + it('Should correctly watch and update based on object state changes', ({ container }) => { + let updateState: (newState: { [key: string]: any }) => void; + function ObjectComponent() { + let state = {}; + let watchedState = {}; + updateState = (newState: { [key: string]: any }) => { + state = newState; + }; + watch(() => { + watchedState = { ...state }; + }); + return
    {JSON.stringify(watchedState)}
    ; + } + render(ObjectComponent, container); + expect(container.innerHTML).toBe('
    {}
    '); + updateState({ key: 'value' }); + expect(container.innerHTML).toBe('
    {"key":"value"}
    '); + }); + + it('Should correctly watch conditional expressions', ({ container }) => { + let updateX: (value: number) => void; + function ConditionalComponent() { + let x = 5; + let result = ''; + updateX = (value: number) => { + x = value; + }; + watch(() => { + result = x > 10 ? 'Greater than 10' : 'Less than or equal to 10'; + }); + return
    {result}
    ; + } + render(ConditionalComponent, container); + expect(container.innerHTML).toBe('
    Less than or equal to 10
    '); + updateX(15); + expect(container.innerHTML).toBe('
    Greater than 10
    '); + }); + + it('Should correctly watch template strings', ({ container }) => { + let updateName: (value: string) => void; + let updateAge: (value: number) => void; + function TemplateStringComponent() { + let name = 'John'; + let age = 30; + let message = ''; + updateName = (value: string) => { + name = value; + }; + updateAge = (value: number) => { + age = value; + }; + watch(() => { + message = `${name} is ${age} years old`; + }); + return
    {message}
    ; + } + render(TemplateStringComponent, container); + expect(container.innerHTML).toBe('
    John is 30 years old
    '); + updateName('Jane'); + updateAge(25); + expect(container.innerHTML).toBe('
    Jane is 25 years old
    '); + }); + + it('Should correctly watch arithmetic operations', ({ container }) => { + let updateX: (value: number) => void; + let updateY: (value: number) => void; + function ArithmeticComponent() { + let x = 5; + let y = 3; + let result = 0; + updateX = (value: number) => { + x = value; + }; + updateY = (value: number) => { + y = value; + }; + watch(() => { + result = x + y * 2 - x / y; + }); + return
    {result}
    ; + } + render(ArithmeticComponent, container); + expect(container.innerHTML).toBe('
    9.333333333333334
    '); + updateX(10); + updateY(5); + expect(container.innerHTML).toBe('
    18
    '); + }); + + it('Should correctly watch property access and indexing', ({ container }) => { + let updateObj: (value: { [key: string]: any }) => void; + let updateArr: (value: number[]) => void; + function AccessComponent() { + let obj = { a: 1, b: 2 }; + let arr = [1, 2, 3]; + let result = 0; + updateObj = (value: { [key: string]: any }) => { + obj = value; + }; + updateArr = (value: number[]) => { + arr = value; + }; + watch(() => { + result = obj.a + arr[1]; + }); + return
    {result}
    ; + } + render(AccessComponent, container); + expect(container.innerHTML).toBe('
    3
    '); + updateObj({ a: 5, b: 6 }); + updateArr([0, 10, 20]); + expect(container.innerHTML).toBe('
    15
    '); + }); + + it('Should correctly watch function calls', ({ container }) => { + let updateX: (value: number) => void; + function FunctionCallComponent() { + let x = 5; + let result = 0; + const square = (n: number) => n * n; + updateX = (value: number) => { + x = value; + }; + watch(() => { + result = square(x); + }); + return
    {result}
    ; + } + render(FunctionCallComponent, container); + expect(container.innerHTML).toBe('
    25
    '); + updateX(10); + expect(container.innerHTML).toBe('
    100
    '); + }); + + it('Should correctly watch various number operations', ({ container }) => { + let updateX: (value: number) => void; + function NumberOpsComponent() { + let x = 16; + let result = ''; + updateX = (value: number) => { + x = value; + }; + watch(() => { + result = `sqrt: ${Math.sqrt(x)}, floor: ${Math.floor(x / 3)}, pow: ${Math.pow(x, 2)}`; + }); + return
    {result}
    ; + } + render(NumberOpsComponent, container); + expect(container.innerHTML).toBe('
    sqrt: 4, floor: 5, pow: 256
    '); + updateX(25); + expect(container.innerHTML).toBe('
    sqrt: 5, floor: 8, pow: 625
    '); + }); + + it('Should correctly watch map operations', ({ container }) => { + let updateArr: (value: number[]) => void; + function MapComponent() { + let arr = [1, 2, 3, 4, 5]; + let result: number[] = []; + updateArr = (value: number[]) => { + arr = value; + }; + watch(() => { + result = arr.map(x => x * 2); + }); + return
    {result.join(', ')}
    ; + } + render(MapComponent, container); + expect(container.innerHTML).toBe('
    2, 4, 6, 8, 10
    '); + updateArr([10, 20, 30]); + expect(container.innerHTML).toBe('
    20, 40, 60
    '); + }); + + it('Should correctly watch multiple variables', ({ container }) => { + let updateX: (value: number) => void; + let updateY: (value: string) => void; + let updateZ: (value: boolean) => void; + function MultiWatchComponent() { + let x = 5; + let y = 'hello'; + let z = true; + let result = ''; + updateX = (value: number) => { + x = value; + }; + updateY = (value: string) => { + y = value; + }; + updateZ = (value: boolean) => { + z = value; + }; + watch(() => { + result = `x: ${x}, y: ${y}, z: ${z}`; + }); + return
    {result}
    ; + } + render(MultiWatchComponent, container); + expect(container.innerHTML).toBe('
    x: 5, y: hello, z: true
    '); + updateX(10); + updateY('world'); + updateZ(false); + expect(container.innerHTML).toBe('
    x: 10, y: world, z: false
    '); + }); +}); diff --git a/packages/inula-next/tsconfig.json b/packages/inula-next/tsconfig.json index 1dc9b4c07aa4aa7f46ea209ad2c55f9a63fb4f2d..636ad3e7098b5a54bdc8f1caaeeabaef0845a6dc 100644 --- a/packages/inula-next/tsconfig.json +++ b/packages/inula-next/tsconfig.json @@ -1,14 +1,14 @@ -{ - "compilerOptions": { - "jsx": "preserve", - "target": "ESNext", - "module": "ESNext", - "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", - "strict": true, - "esModuleInterop": true - }, - "ts-node": { - "esm": true - } -} +{ + "compilerOptions": { + "jsx": "preserve", + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true + }, + "ts-node": { + "esm": true + } +} diff --git a/packages/inula-next/vitest.config.ts b/packages/inula-next/vitest.config.ts index fa1d5812a173d559f9969376e97ee55f7fdd80f2..39bc672b72680e8ed8682348cc628679fca740ed 100644 --- a/packages/inula-next/vitest.config.ts +++ b/packages/inula-next/vitest.config.ts @@ -1,39 +1,39 @@ -/* - * 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. - */ - -// vitest.config.ts -import { defineConfig } from 'vitest/config'; -import inula from '@openinula/vite-plugin-inula-next'; -import * as path from 'node:path'; - -export default defineConfig({ - esbuild: { - jsx: 'preserve', - }, - resolve: { - alias: { - '@openinula/next': path.resolve(__dirname, 'src'), - }, - conditions: ['dev'], - }, - plugins: [ - inula({ - excludeFiles: '**/src/**/*{js,jsx,ts,tsx}', - }), - ], - test: { - environment: 'jsdom', // or 'jsdom', 'node' - }, -}); +/* + * 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. + */ + +// vitest.config.ts +import { defineConfig } from 'vitest/config'; +import inula from '@openinula/vite-plugin-inula-next'; +import * as path from 'node:path'; + +export default defineConfig({ + esbuild: { + jsx: 'preserve', + }, + resolve: { + alias: { + '@openinula/next': path.resolve(__dirname, 'src'), + }, + conditions: ['dev'], + }, + plugins: [ + inula({ + excludeFiles: '**/src/**/*{js,jsx,ts,tsx}', + }), + ], + test: { + environment: 'jsdom', // or 'jsdom', 'node' + }, +}); diff --git a/packages/transpiler/babel-api/README.md b/packages/transpiler/babel-api/README.md index ef06c4f49f59167ba2b3775d7620dcfa8207055a..f013598e829b9d9329602f74644c4c22116a8d6d 100644 --- a/packages/transpiler/babel-api/README.md +++ b/packages/transpiler/babel-api/README.md @@ -1,24 +1,24 @@ -# @openinlua/babel-api -A package that encapsulates the babel API for use in the transpiler. - -To implement the dependency injection pattern, the package exports a function that registers the babel API in the -transpiler. - -```ts -import { registerBabelAPI } from '@openinlua/babel-api'; - -function plugin(api: typeof babel) { - registerBabelAPI(api); - - // Your babel plugin code here. -} -``` - -And then you can import to use it. -> types can use as a `type` or as a `namespace` for the babel API. - -```ts -import { types as t } from '@openinlua/babel-api'; - -t.isIdentifier(node as t.Node); -``` +# @openinlua/babel-api +A package that encapsulates the babel API for use in the transpiler. + +To implement the dependency injection pattern, the package exports a function that registers the babel API in the +transpiler. + +```ts +import { registerBabelAPI } from '@openinlua/babel-api'; + +function plugin(api: typeof babel) { + registerBabelAPI(api); + + // Your babel plugin code here. +} +``` + +And then you can import to use it. +> types can use as a `type` or as a `namespace` for the babel API. + +```ts +import { types as t } from '@openinlua/babel-api'; + +t.isIdentifier(node as t.Node); +``` diff --git a/packages/transpiler/babel-api/src/index.d.ts b/packages/transpiler/babel-api/src/index.d.ts index 523f1d497f76420d90e2a994b1fbb376c2abab4c..d352a121dc8a2ed183406711294c3caab0886290 100644 --- a/packages/transpiler/babel-api/src/index.d.ts +++ b/packages/transpiler/babel-api/src/index.d.ts @@ -1,10 +1,10 @@ -import type babel from '@babel/core'; - -// use .d.ts to satisfy the type check -export * as types from '@babel/types'; - -export declare function register(api: typeof babel): void; - -export declare function getBabelApi(): typeof babel; - -export { traverse } from '@babel/core'; +import type babel from '@babel/core'; + +// use .d.ts to satisfy the type check +export * as types from '@babel/types'; + +export declare function register(api: typeof babel): void; + +export declare function getBabelApi(): typeof babel; + +export { traverse } from '@babel/core'; diff --git a/packages/transpiler/babel-api/src/index.mjs b/packages/transpiler/babel-api/src/index.mjs index 08c18bf9dbc5f7521603962e49d095d6133fd076..42e68b70476c7193fa87997288ef791ed1109d3e 100644 --- a/packages/transpiler/babel-api/src/index.mjs +++ b/packages/transpiler/babel-api/src/index.mjs @@ -1,42 +1,42 @@ -/** @type {null | typeof import('@babel/core').types} */ -let _t = null; -/** @type {null | typeof import('@babel/core')} */ -let babelApi = null; - -/** - * @param {import('@babel/core')} api - */ -export const register = api => { - babelApi = api; - _t = api.types; -}; - -/** - * @returns {typeof import('@babel/core')} - */ -export const getBabelApi = () => { - if (!babelApi) { - throw new Error('Please call register() before using the babel api'); - } - return babelApi; -}; - -export function traverse(node, visitor) { - getBabelApi().traverse(node, visitor); -} - -export const types = new Proxy( - {}, - { - get: (_, p, receiver) => { - if (!_t) { - throw new Error('Please call register() before using the babel types'); - } - - if (p in _t) { - return Reflect.get(_t, p, receiver); - } - return undefined; - }, - } -); +/** @type {null | typeof import('@babel/core').types} */ +let _t = null; +/** @type {null | typeof import('@babel/core')} */ +let babelApi = null; + +/** + * @param {import('@babel/core')} api + */ +export const register = api => { + babelApi = api; + _t = api.types; +}; + +/** + * @returns {typeof import('@babel/core')} + */ +export const getBabelApi = () => { + if (!babelApi) { + throw new Error('Please call register() before using the babel api'); + } + return babelApi; +}; + +export function traverse(node, visitor) { + getBabelApi().traverse(node, visitor); +} + +export const types = new Proxy( + {}, + { + get: (_, p, receiver) => { + if (!_t) { + throw new Error('Please call register() before using the babel types'); + } + + if (p in _t) { + return Reflect.get(_t, p, receiver); + } + return undefined; + }, + } +); diff --git a/packages/transpiler/babel-inula-next-core/CHANGELOG.md b/packages/transpiler/babel-inula-next-core/CHANGELOG.md index 263ea4f54ee0de8e03b1f3bad831879c4855f1b3..0bce80a3e79476a18ddfbc802680465e376899eb 100644 --- a/packages/transpiler/babel-inula-next-core/CHANGELOG.md +++ b/packages/transpiler/babel-inula-next-core/CHANGELOG.md @@ -1,14 +1,25 @@ -# babel-preset-inula-next - -## 0.0.3 - -### Patch Changes - -- Updated dependencies - - @openinula/class-transformer@0.0.2 - -## 0.0.2 - -### Patch Changes - -- 2f9d373: feat: change babel import +# babel-preset-inula-next + +## 0.0.4 + +### Patch Changes + +- support hook +- Updated dependencies + - @openinula/reactivity-parser@0.0.2 + - @openinula/view-generator@1.0.0 + - @openinula/jsx-view-parser@0.0.2 + - @openinula/babel-api@1.0.1 + +## 0.0.3 + +### Patch Changes + +- Updated dependencies + - @openinula/class-transformer@0.0.2 + +## 0.0.2 + +### Patch Changes + +- 2f9d373: feat: change babel import diff --git a/packages/transpiler/babel-inula-next-core/package.json b/packages/transpiler/babel-inula-next-core/package.json index 56e8d4e5826229df3068ffcb4b8e1fc8b075f892..0eb17b11b08cc5ee32139093767051bde9adbf32 100644 --- a/packages/transpiler/babel-inula-next-core/package.json +++ b/packages/transpiler/babel-inula-next-core/package.json @@ -1,60 +1,60 @@ -{ - "name": "@openinula/babel-preset-inula-next", - "version": "0.0.0-SNAPSHOT", - "keywords": [ - "Inula-Next", - "babel-preset" - ], - "license": "MIT", - "files": [ - "dist" - ], - "type": "module", - "main": "dist/index.js", - "module": "dist/index.js", - "typings": "dist/index.d.ts", - "scripts": { - "build": "tsup --sourcemap", - "type-check": "tsc --noEmit", - "test": "vitest" - }, - "devDependencies": { - "@types/babel__core": "^7.20.5", - "@types/node": "^20.10.5", - "@vitest/coverage-v8": "^1.6.0", - "sinon": "^18.0.0", - "tsup": "^6.7.0", - "typescript": "^5.3.2" - }, - "dependencies": { - "@babel/core": "^7.23.3", - "@babel/generator": "^7.23.6", - "@babel/parser": "^7.24.4", - "@babel/plugin-syntax-decorators": "^7.23.3", - "@babel/plugin-syntax-jsx": "7.16.7", - "@babel/plugin-syntax-typescript": "^7.23.3", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", - "@openinula/babel-api": "workspace:*", - "@openinula/jsx-view-parser": "workspace:*", - "@openinula/reactivity-parser": "workspace:*", - "@openinula/view-generator": "workspace:*", - "@types/babel-types": "^7.0.15", - "@types/babel__generator": "^7.6.8", - "@types/babel__parser": "^7.1.1", - "@types/babel__traverse": "^7.6.8", - "minimatch": "^9.0.3", - "vitest": "^1.4.0" - }, - "tsup": { - "entry": [ - "src/index.ts" - ], - "format": [ - "cjs", - "esm" - ], - "clean": true, - "dts": true - } -} +{ + "name": "@openinula/babel-preset-inula-next", + "version": "0.0.0-SNAPSHOT", + "keywords": [ + "Inula-Next", + "babel-preset" + ], + "license": "MIT", + "files": [ + "dist" + ], + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "typings": "dist/index.d.ts", + "scripts": { + "build": "tsup --sourcemap", + "type-check": "tsc --noEmit", + "test": "vitest run" + }, + "devDependencies": { + "@types/babel__core": "^7.20.5", + "@types/node": "^20.10.5", + "@vitest/coverage-v8": "^1.6.0", + "sinon": "^18.0.0", + "tsup": "^6.7.0", + "typescript": "^5.3.2" + }, + "dependencies": { + "@babel/core": "^7.23.3", + "@babel/generator": "^7.23.6", + "@babel/parser": "^7.24.4", + "@babel/plugin-syntax-decorators": "^7.23.3", + "@babel/plugin-syntax-jsx": "7.16.7", + "@babel/plugin-syntax-typescript": "^7.23.3", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0", + "@openinula/babel-api": "workspace:*", + "@openinula/jsx-view-parser": "workspace:*", + "@openinula/reactivity-parser": "workspace:*", + "@openinula/view-generator": "workspace:*", + "@types/babel-types": "^7.0.15", + "@types/babel__generator": "^7.6.8", + "@types/babel__parser": "^7.1.1", + "@types/babel__traverse": "^7.6.8", + "minimatch": "^9.0.3", + "vitest": "^1.4.0" + }, + "tsup": { + "entry": [ + "src/index.ts" + ], + "format": [ + "cjs", + "esm" + ], + "clean": true, + "dts": true + } +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/functionalMacroAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/functionalMacroAnalyze.ts index 65da5eae829b693262143a3510ffcd62691bb355..96db95a1dba3e268ba9d001810dfa8af5083a385 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/functionalMacroAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/functionalMacroAnalyze.ts @@ -1,85 +1,85 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { NodePath } from '@babel/core'; -import { LifeCycle, Visitor } from '../types'; -import { types as t } from '@openinula/babel-api'; -import { DID_MOUNT, DID_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../../constants'; -import { extractFnFromMacro, getFnBodyPath } from '../../utils'; -import { extractFnBody } from '../utils'; - -function isLifeCycleName(name: string): name is LifeCycle { - return [WILL_MOUNT, DID_MOUNT, WILL_UNMOUNT, DID_UNMOUNT].includes(name); -} - -/** - * Analyze the functional macro in the function component - * 1. lifecycle - * 1. willMount - * 2. didMount - * 3. willUnMount - * 4. didUnmount - * 2. watch - */ -export function functionalMacroAnalyze(): Visitor { - return { - ExpressionStatement(path: NodePath, { builder }) { - const expression = path.get('expression'); - if (expression.isCallExpression()) { - const callee = expression.get('callee'); - if (callee.isIdentifier()) { - const calleeName = callee.node.name; - // lifecycle - if (isLifeCycleName(calleeName)) { - const fnPath = extractFnFromMacro(expression, calleeName); - builder.addLifecycle(calleeName, extractFnBody(fnPath.node)); - return; - } - - // watch - if (calleeName === WATCH) { - const fnPath = extractFnFromMacro(expression, WATCH); - const depsPath = getWatchDeps(expression); - - const dependency = builder.getDependency((depsPath ?? fnPath).node); - if (dependency.allDepBits.length) { - builder.addWatch(fnPath, dependency); - } else { - builder.addLifecycle(WILL_MOUNT, extractFnBody(fnPath.node)); - } - return; - } - } - } - - builder.addWillMount(path.node); - }, - }; -} - -function getWatchDeps(callExpression: NodePath) { - const args = callExpression.get('arguments'); - if (!args[1]) { - return null; - } - - let deps: null | NodePath = null; - if (args[1].isArrayExpression()) { - deps = args[1]; - } else { - console.error('watch deps should be an array expression'); - } - return deps; -} +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { NodePath } from '@babel/core'; +import { LifeCycle, Visitor } from '../types'; +import { types as t } from '@openinula/babel-api'; +import { DID_MOUNT, DID_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../../constants'; +import { extractFnFromMacro, getFnBodyPath } from '../../utils'; +import { extractFnBody } from '../utils'; + +function isLifeCycleName(name: string): name is LifeCycle { + return [WILL_MOUNT, DID_MOUNT, WILL_UNMOUNT, DID_UNMOUNT].includes(name); +} + +/** + * Analyze the functional macro in the function component + * 1. lifecycle + * 1. willMount + * 2. didMount + * 3. willUnMount + * 4. didUnmount + * 2. watch + */ +export function functionalMacroAnalyze(): Visitor { + return { + ExpressionStatement(path: NodePath, { builder }) { + const expression = path.get('expression'); + if (expression.isCallExpression()) { + const callee = expression.get('callee'); + if (callee.isIdentifier()) { + const calleeName = callee.node.name; + // lifecycle + if (isLifeCycleName(calleeName)) { + const fnPath = extractFnFromMacro(expression, calleeName); + builder.addLifecycle(calleeName, extractFnBody(fnPath.node)); + return; + } + + // watch + if (calleeName === WATCH) { + const fnPath = extractFnFromMacro(expression, WATCH); + const depsPath = getWatchDeps(expression); + + const dependency = builder.getDependency((depsPath ?? fnPath).node); + if (dependency.allDepBits.length) { + builder.addWatch(fnPath, dependency); + } else { + builder.addLifecycle(WILL_MOUNT, extractFnBody(fnPath.node)); + } + return; + } + } + } + + builder.addWillMount(path.node); + }, + }; +} + +function getWatchDeps(callExpression: NodePath) { + const args = callExpression.get('arguments'); + if (!args[1]) { + return null; + } + + let deps: null | NodePath = null; + if (args[1].isArrayExpression()) { + deps = args[1]; + } else { + console.error('watch deps should be an array expression'); + } + return deps; +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/hookAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/hookAnalyze.ts index aeb651cf18370814c2773f420aba64afb55c5f78..e8d0e604b720676e75160f5c2f9b80390abe7c58 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/hookAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/hookAnalyze.ts @@ -1,32 +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 { Visitor } from '../types'; -import { type NodePath } from '@babel/core'; -import { types as t } from '@openinula/babel-api'; - -/** - * Analyze the return in the hook - */ -export function hookReturnAnalyze(): Visitor { - return { - ReturnStatement(path: NodePath, { builder }) { - const returnedNode = path.node.argument; - if (returnedNode) { - builder.setReturnValue(returnedNode); - } - }, - }; -} +/* + * 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 { Visitor } from '../types'; +import { type NodePath } from '@babel/core'; +import { types as t } from '@openinula/babel-api'; + +/** + * Analyze the return in the hook + */ +export function hookReturnAnalyze(): Visitor { + return { + ReturnStatement(path: NodePath, { builder }) { + const returnedNode = path.node.argument; + if (returnedNode) { + builder.setReturnValue(returnedNode); + } + }, + }; +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/variablesAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/variablesAnalyze.ts index 4e9c1f8b9f575d2351be01158a63c91e28b5f8b3..e04584b0b28fa9fc0e3ed05d7aa13992f385d8d4 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/variablesAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/variablesAnalyze.ts @@ -1,129 +1,129 @@ -/* - * 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 { AnalyzeContext, Visitor } from '../types'; -import { isStaticValue, isValidPath } from '../utils'; -import { type NodePath } from '@babel/core'; -import { importMap } from '../../constants'; -import { analyzeUnitOfWork } from '../index'; -import { types as t } from '@openinula/babel-api'; -import { isCompPath } from '../../utils'; -import { IRBuilder } from '../IRBuilder'; - -/** - * collect all properties and methods from the node - * and analyze the dependencies of the properties - * @returns - */ -export function variablesAnalyze(): Visitor { - return { - VariableDeclaration(path: NodePath, ctx) { - const { builder } = ctx; - const declarations = path.get('declarations'); - // iterate the declarations - declarations.forEach(declaration => { - const id = declaration.get('id'); - assertIdentifier(id); - // --- properties: the state / computed / plain properties / methods --- - const init = declaration.get('init'); - const kind = path.node.kind; - - // Check if the variable can't be modified - if (kind === 'const' && isStaticValue(init.node)) { - builder.addPlainVariable(t.variableDeclaration('const', [declaration.node])); - return; - } - - if (!isValidPath(init)) { - resolveUninitializedVariable(kind, builder, id, declaration.node); - return; - } - - // Handle the subcomponent, should like Component(() => {}) - if (init.isCallExpression() && isCompPath(init)) { - resolveSubComponent(init, builder, id, ctx); - return; - } - - // ensure evert jsx slice call expression can found responding sub-component - assertJSXSliceIsValid(path, builder.checkSubComponent.bind(builder)); - - builder.addVariable({ - name: id.node.name, - value: init.node, - kind: path.node.kind, - }); - }); - }, - FunctionDeclaration(path: NodePath, { builder }) { - builder.addPlainVariable(path.node); - }, - }; -} - -function assertIdentifier(id: NodePath): asserts id is NodePath { - if (!id.isIdentifier()) { - throw new Error(`${id.node.type} is not valid initial value type for state`); - } -} - -function assertJSXSliceIsValid(path: NodePath, checker: (name: string) => boolean) { - path.traverse({ - CallExpression(callPath) { - const callee = callPath.node.callee; - if (t.isIdentifier(callee) && callee.name === importMap.Comp) { - const subCompIdPath = callPath.get('arguments')[0]; - if (!subCompIdPath.isIdentifier()) { - throw Error('invalid jsx slice'); - } - const subCompName = subCompIdPath.node.name; - if (!checker(subCompName)) { - throw Error(`Sub component not found: ${subCompName}`); - } - } - }, - }); -} - -function resolveUninitializedVariable( - kind: 'var' | 'let' | 'const' | 'using' | 'await using', - builder: IRBuilder, - id: NodePath, - node: Array[number] -) { - if (kind === 'const') { - builder.addPlainVariable(t.variableDeclaration('const', [node])); - } - builder.addVariable({ - name: id.node.name, - value: null, - kind, - }); -} - -function resolveSubComponent( - init: NodePath, - builder: IRBuilder, - id: NodePath, - ctx: AnalyzeContext -) { - const fnNode = init.get('arguments')[0] as NodePath | NodePath; - - builder.startSubComponent(id.node.name, fnNode); - - analyzeUnitOfWork(id.node.name, fnNode, ctx); - - builder.endSubComponent(); -} +/* + * 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 { AnalyzeContext, Visitor } from '../types'; +import { isStaticValue, isValidPath } from '../utils'; +import { type NodePath } from '@babel/core'; +import { importMap } from '../../constants'; +import { analyzeUnitOfWork } from '../index'; +import { types as t } from '@openinula/babel-api'; +import { isCompPath } from '../../utils'; +import { IRBuilder } from '../IRBuilder'; + +/** + * collect all properties and methods from the node + * and analyze the dependencies of the properties + * @returns + */ +export function variablesAnalyze(): Visitor { + return { + VariableDeclaration(path: NodePath, ctx) { + const { builder } = ctx; + const declarations = path.get('declarations'); + // iterate the declarations + declarations.forEach(declaration => { + const id = declaration.get('id'); + assertIdentifier(id); + // --- properties: the state / computed / plain properties / methods --- + const init = declaration.get('init'); + const kind = path.node.kind; + + // Check if the variable can't be modified + if (kind === 'const' && isStaticValue(init.node)) { + builder.addPlainVariable(t.variableDeclaration('const', [declaration.node])); + return; + } + + if (!isValidPath(init)) { + resolveUninitializedVariable(kind, builder, id, declaration.node); + return; + } + + // Handle the subcomponent, should like Component(() => {}) + if (init.isCallExpression() && isCompPath(init)) { + resolveSubComponent(init, builder, id, ctx); + return; + } + + // ensure evert jsx slice call expression can found responding sub-component + assertJSXSliceIsValid(path, builder.checkSubComponent.bind(builder)); + + builder.addVariable({ + name: id.node.name, + value: init.node, + kind: path.node.kind, + }); + }); + }, + FunctionDeclaration(path: NodePath, { builder }) { + builder.addPlainVariable(path.node); + }, + }; +} + +function assertIdentifier(id: NodePath): asserts id is NodePath { + if (!id.isIdentifier()) { + throw new Error(`${id.node.type} is not valid initial value type for state`); + } +} + +function assertJSXSliceIsValid(path: NodePath, checker: (name: string) => boolean) { + path.traverse({ + CallExpression(callPath) { + const callee = callPath.node.callee; + if (t.isIdentifier(callee) && callee.name === importMap.Comp) { + const subCompIdPath = callPath.get('arguments')[0]; + if (!subCompIdPath.isIdentifier()) { + throw Error('invalid jsx slice'); + } + const subCompName = subCompIdPath.node.name; + if (!checker(subCompName)) { + throw Error(`Sub component not found: ${subCompName}`); + } + } + }, + }); +} + +function resolveUninitializedVariable( + kind: 'var' | 'let' | 'const' | 'using' | 'await using', + builder: IRBuilder, + id: NodePath, + node: Array[number] +) { + if (kind === 'const') { + builder.addPlainVariable(t.variableDeclaration('const', [node])); + } + builder.addVariable({ + name: id.node.name, + value: null, + kind, + }); +} + +function resolveSubComponent( + init: NodePath, + builder: IRBuilder, + id: NodePath, + ctx: AnalyzeContext +) { + const fnNode = init.get('arguments')[0] as NodePath | NodePath; + + builder.startSubComponent(id.node.name, fnNode); + + analyzeUnitOfWork(id.node.name, fnNode, ctx); + + builder.endSubComponent(); +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/viewAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/viewAnalyze.ts index 58597636359b4713ba65e3d84c92b0612b5f02a1..4ee6d4608753a166eb4dee85e04ae56cf04b53f2 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/viewAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/Analyzers/viewAnalyze.ts @@ -1,32 +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 { Visitor } from '../types'; -import { type NodePath } from '@babel/core'; -import { types as t } from '@openinula/babel-api'; - -/** - * Analyze the watch in the function component - */ -export function viewAnalyze(): Visitor { - return { - ReturnStatement(path: NodePath, { builder }) { - const returnedPath = path.get('argument'); - if (returnedPath.isJSXElement() || returnedPath.isJSXFragment()) { - builder.setViewChild(returnedPath.node); - } - }, - }; -} +/* + * 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 { Visitor } from '../types'; +import { type NodePath } from '@babel/core'; +import { types as t } from '@openinula/babel-api'; + +/** + * Analyze the watch in the function component + */ +export function viewAnalyze(): Visitor { + return { + ReturnStatement(path: NodePath, { builder }) { + const returnedPath = path.get('argument'); + if (returnedPath.isJSXElement() || returnedPath.isJSXFragment()) { + builder.setViewChild(returnedPath.node); + } + }, + }; +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/IRBuilder.ts b/packages/transpiler/babel-inula-next-core/src/analyze/IRBuilder.ts index 6af05421330656ab6238e5fad27cc2ac3006223b..48727b172b7bb4288eaa4fa715c67655b0ea20ea 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/IRBuilder.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/IRBuilder.ts @@ -1,156 +1,156 @@ -/* - * 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 { - BaseVariable, - ComponentNode, - CompOrHook, - Dependency, - FunctionalExpression, - HookNode, - IRNode, - LifeCycle, - PlainVariable, -} from './types'; -import { createIRNode } from './nodeFactory'; -import type { NodePath } from '@babel/core'; -import { getBabelApi, types as t } from '@openinula/babel-api'; -import { COMPONENT, reactivityFuncNames, WILL_MOUNT } from '../constants'; -import { Bitmap, getDependenciesFromNode, parseReactivity } from '@openinula/reactivity-parser'; -import { assertComponentNode, assertHookNode } from './utils'; -import { parseView as parseJSX } from '@openinula/jsx-view-parser'; -import { pruneUnusedState } from './pruneUnusedState'; - -export class IRBuilder { - #current: HookNode | ComponentNode; - readonly #htmlTags: string[]; - - constructor(name: string, type: CompOrHook, fnNode: NodePath, htmlTags: string[]) { - this.#current = createIRNode(name, type, fnNode); - this.#htmlTags = htmlTags; - } - - accumulateUsedBit(allDepBits: Bitmap[]) { - this.#current.usedBit |= allDepBits.reduce((acc, cur) => acc | cur, 0); - } - - addVariable(varInfo: BaseVariable) { - const value = varInfo.value; - const dependency = - !value || t.isArrowFunctionExpression(value) || t.isFunctionExpression(value) ? null : this.getDependency(value); - - // The index of the variable in the availableVariables - const idx = this.#current.availableVariables.length; - const bit = 1 << idx; - const allDepBits = dependency?.allDepBits; - - if (allDepBits?.length) { - this.accumulateUsedBit(allDepBits); - } - this.#current._reactiveBitMap.set(varInfo.name, bit); - this.#current.variables.push({ - ...varInfo, - type: 'reactive', - bit, - level: this.#current.level, - dependency: allDepBits?.length ? dependency : null, - }); - } - - addPlainVariable(value: PlainVariable['value']) { - this.#current.variables.push({ value, type: 'plain' }); - } - - addSubComponent(subComp: IRNode) { - this.#current.usedBit |= subComp.usedBit; - this.#current.variables.push({ ...subComp, type: 'subComp' }); - } - - addLifecycle(lifeCycle: LifeCycle, block: t.Statement) { - const compLifecycle = this.#current.lifecycle; - if (!compLifecycle[lifeCycle]) { - compLifecycle[lifeCycle] = []; - } - compLifecycle[lifeCycle]!.push(block); - } - - addWillMount(stmt: t.Statement) { - this.addLifecycle(WILL_MOUNT, stmt); - } - - addWatch(callback: NodePath | NodePath, dependency: Dependency) { - // if watch not exist, create a new one - if (!this.#current.watch) { - this.#current.watch = []; - } - this.accumulateUsedBit(dependency.allDepBits); - this.#current.watch.push({ - callback, - dependency: dependency.allDepBits.length ? dependency : null, - }); - } - - setViewChild(viewNode: t.JSXElement | t.JSXFragment) { - assertComponentNode(this.#current); - - const viewUnits = parseJSX(viewNode, { - babelApi: getBabelApi(), - htmlTags: this.#htmlTags, - parseTemplate: false, - }); - - const [viewParticles, usedBit] = parseReactivity(viewUnits, { - babelApi: getBabelApi(), - depMaskMap: this.#current._reactiveBitMap, - reactivityFuncNames, - }); - - // TODO: Maybe we should merge - this.#current.usedBit |= usedBit; - this.#current.children = viewParticles; - } - - setReturnValue(expression: t.Expression) { - assertHookNode(this.#current); - const dependency = getDependenciesFromNode(expression, this.#current._reactiveBitMap, reactivityFuncNames); - - this.accumulateUsedBit(dependency.allDepBits); - this.#current.children = { value: expression, ...dependency }; - } - - getDependency(node: t.Expression | t.Statement) { - return getDependenciesFromNode(node, this.#current._reactiveBitMap, reactivityFuncNames); - } - - checkSubComponent(subCompName: string) { - return !!this.#current.variables.find(sub => sub.type === 'subComp' && sub.name === subCompName); - } - - startSubComponent(name: string, fnNode: NodePath | NodePath) { - assertComponentNode(this.#current); - this.#current = createIRNode(name, COMPONENT, fnNode, this.#current); - } - - endSubComponent() { - const subComp = this.#current; - this.#current = this.#current.parent!; - this.addSubComponent(subComp); - } - - build() { - pruneUnusedState(this.#current); - return this.#current; - } -} +/* + * 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 { + BaseVariable, + ComponentNode, + CompOrHook, + Dependency, + FunctionalExpression, + HookNode, + IRNode, + LifeCycle, + PlainVariable, +} from './types'; +import { createIRNode } from './nodeFactory'; +import type { NodePath } from '@babel/core'; +import { getBabelApi, types as t } from '@openinula/babel-api'; +import { COMPONENT, reactivityFuncNames, WILL_MOUNT } from '../constants'; +import { Bitmap, getDependenciesFromNode, parseReactivity } from '@openinula/reactivity-parser'; +import { assertComponentNode, assertHookNode } from './utils'; +import { parseView as parseJSX } from '@openinula/jsx-view-parser'; +import { pruneUnusedState } from './pruneUnusedState'; + +export class IRBuilder { + #current: HookNode | ComponentNode; + readonly #htmlTags: string[]; + + constructor(name: string, type: CompOrHook, fnNode: NodePath, htmlTags: string[]) { + this.#current = createIRNode(name, type, fnNode); + this.#htmlTags = htmlTags; + } + + accumulateUsedBit(allDepBits: Bitmap[]) { + this.#current.usedBit |= allDepBits.reduce((acc, cur) => acc | cur, 0); + } + + addVariable(varInfo: BaseVariable) { + const value = varInfo.value; + const dependency = + !value || t.isArrowFunctionExpression(value) || t.isFunctionExpression(value) ? null : this.getDependency(value); + + // The index of the variable in the availableVariables + const idx = this.#current.availableVariables.length; + const bit = 1 << idx; + const allDepBits = dependency?.allDepBits; + + if (allDepBits?.length) { + this.accumulateUsedBit(allDepBits); + } + this.#current._reactiveBitMap.set(varInfo.name, bit); + this.#current.variables.push({ + ...varInfo, + type: 'reactive', + bit, + level: this.#current.level, + dependency: allDepBits?.length ? dependency : null, + }); + } + + addPlainVariable(value: PlainVariable['value']) { + this.#current.variables.push({ value, type: 'plain' }); + } + + addSubComponent(subComp: IRNode) { + this.#current.usedBit |= subComp.usedBit; + this.#current.variables.push({ ...subComp, type: 'subComp' }); + } + + addLifecycle(lifeCycle: LifeCycle, block: t.Statement) { + const compLifecycle = this.#current.lifecycle; + if (!compLifecycle[lifeCycle]) { + compLifecycle[lifeCycle] = []; + } + compLifecycle[lifeCycle]!.push(block); + } + + addWillMount(stmt: t.Statement) { + this.addLifecycle(WILL_MOUNT, stmt); + } + + addWatch(callback: NodePath | NodePath, dependency: Dependency) { + // if watch not exist, create a new one + if (!this.#current.watch) { + this.#current.watch = []; + } + this.accumulateUsedBit(dependency.allDepBits); + this.#current.watch.push({ + callback, + dependency: dependency.allDepBits.length ? dependency : null, + }); + } + + setViewChild(viewNode: t.JSXElement | t.JSXFragment) { + assertComponentNode(this.#current); + + const viewUnits = parseJSX(viewNode, { + babelApi: getBabelApi(), + htmlTags: this.#htmlTags, + parseTemplate: false, + }); + + const [viewParticles, usedBit] = parseReactivity(viewUnits, { + babelApi: getBabelApi(), + depMaskMap: this.#current._reactiveBitMap, + reactivityFuncNames, + }); + + // TODO: Maybe we should merge + this.#current.usedBit |= usedBit; + this.#current.children = viewParticles; + } + + setReturnValue(expression: t.Expression) { + assertHookNode(this.#current); + const dependency = getDependenciesFromNode(expression, this.#current._reactiveBitMap, reactivityFuncNames); + + this.accumulateUsedBit(dependency.allDepBits); + this.#current.children = { value: expression, ...dependency }; + } + + getDependency(node: t.Expression | t.Statement) { + return getDependenciesFromNode(node, this.#current._reactiveBitMap, reactivityFuncNames); + } + + checkSubComponent(subCompName: string) { + return !!this.#current.variables.find(sub => sub.type === 'subComp' && sub.name === subCompName); + } + + startSubComponent(name: string, fnNode: NodePath | NodePath) { + assertComponentNode(this.#current); + this.#current = createIRNode(name, COMPONENT, fnNode, this.#current); + } + + endSubComponent() { + const subComp = this.#current; + this.#current = this.#current.parent!; + this.addSubComponent(subComp); + } + + build() { + pruneUnusedState(this.#current); + return this.#current; + } +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/index.ts b/packages/transpiler/babel-inula-next-core/src/analyze/index.ts index 9750bce5f0cb00e1061f4a0c53f9d9f2048a5c52..a07b7b9a4a22dcabdd512ce45c0d3e55be33fbb7 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/index.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/index.ts @@ -1,96 +1,96 @@ -import { type NodePath } from '@babel/core'; -import { AnalyzeContext, Analyzer, CompOrHook, FunctionalExpression, Visitor } from './types'; -import { variablesAnalyze } from './Analyzers/variablesAnalyze'; -import { functionalMacroAnalyze } from './Analyzers/functionalMacroAnalyze'; -import { getFnBodyPath } from '../utils'; -import { viewAnalyze } from './Analyzers/viewAnalyze'; -import { COMPONENT, HOOK } from '../constants'; -import { types as t } from '@openinula/babel-api'; -import { hookReturnAnalyze } from './Analyzers/hookAnalyze'; -import { IRBuilder } from './IRBuilder'; - -const compBuiltinAnalyzers = [variablesAnalyze, functionalMacroAnalyze, viewAnalyze]; -const hookBuiltinAnalyzers = [variablesAnalyze, functionalMacroAnalyze, hookReturnAnalyze]; - -function mergeVisitor(...visitors: Analyzer[]): Visitor { - return visitors.reduce>((acc, cur) => { - return { ...acc, ...cur() }; - }, {}); -} - -// walk through the body a function (maybe a component or a hook) -export function analyzeUnitOfWork(name: string, fnNode: NodePath, context: AnalyzeContext) { - const { builder, analyzers } = context; - const visitor = mergeVisitor(...analyzers); - - // --- analyze the function props --- - const params = fnNode.get('params'); - const props = params[0]; - if (props) { - if (props.isObjectPattern()) { - props.get('properties').forEach(prop => { - visitor.Prop?.(prop, context); - }); - } else { - throw new Error(`Component ${name}: The first parameter of the function component must be an object pattern`); - } - } - - // --- analyze the function body --- - const bodyStatements = getFnBodyPath(fnNode).get('body'); - for (let i = 0; i < bodyStatements.length; i++) { - const path = bodyStatements[i]; - - const type = path.node.type; - - const visit = visitor[type]; - if (visit) { - // TODO: More type safe way to handle this - visit(path as unknown as any, context); - } else { - builder.addWillMount(path.node); - } - - if (path.isReturnStatement()) { - visitor.ReturnStatement?.(path, context); - break; - } - } -} - -/** - * The process of analyzing the component - * 1. identify the component - * 2. identify the jsx slice in the component - * 2. identify the component's props, including children, alias, and default value - * 3. analyze the early return of the component, build into the branch - * - * @param type - * @param fnName - * @param path - * @param options - */ -export function analyze( - type: CompOrHook, - fnName: string, - path: NodePath, - options: { customAnalyzers?: Analyzer[]; htmlTags: string[] } -) { - const analyzers = options?.customAnalyzers ? options.customAnalyzers : getBuiltinAnalyzers(type); - - const builder = new IRBuilder(fnName, type, path, options.htmlTags); - - analyzeUnitOfWork(fnName, path, { builder, analyzers }); - - return builder.build(); -} - -function getBuiltinAnalyzers(type: CompOrHook) { - if (type === COMPONENT) { - return compBuiltinAnalyzers; - } - if (type === HOOK) { - return hookBuiltinAnalyzers; - } - throw new Error('Unsupported type to analyze'); -} +import { type NodePath } from '@babel/core'; +import { AnalyzeContext, Analyzer, CompOrHook, FunctionalExpression, Visitor } from './types'; +import { variablesAnalyze } from './Analyzers/variablesAnalyze'; +import { functionalMacroAnalyze } from './Analyzers/functionalMacroAnalyze'; +import { getFnBodyPath } from '../utils'; +import { viewAnalyze } from './Analyzers/viewAnalyze'; +import { COMPONENT, HOOK } from '../constants'; +import { types as t } from '@openinula/babel-api'; +import { hookReturnAnalyze } from './Analyzers/hookAnalyze'; +import { IRBuilder } from './IRBuilder'; + +const compBuiltinAnalyzers = [variablesAnalyze, functionalMacroAnalyze, viewAnalyze]; +const hookBuiltinAnalyzers = [variablesAnalyze, functionalMacroAnalyze, hookReturnAnalyze]; + +function mergeVisitor(...visitors: Analyzer[]): Visitor { + return visitors.reduce>((acc, cur) => { + return { ...acc, ...cur() }; + }, {}); +} + +// walk through the body a function (maybe a component or a hook) +export function analyzeUnitOfWork(name: string, fnNode: NodePath, context: AnalyzeContext) { + const { builder, analyzers } = context; + const visitor = mergeVisitor(...analyzers); + + // --- analyze the function props --- + const params = fnNode.get('params'); + const props = params[0]; + if (props) { + if (props.isObjectPattern()) { + props.get('properties').forEach(prop => { + visitor.Prop?.(prop, context); + }); + } else { + throw new Error(`Component ${name}: The first parameter of the function component must be an object pattern`); + } + } + + // --- analyze the function body --- + const bodyStatements = getFnBodyPath(fnNode).get('body'); + for (let i = 0; i < bodyStatements.length; i++) { + const path = bodyStatements[i]; + + const type = path.node.type; + + const visit = visitor[type]; + if (visit) { + // TODO: More type safe way to handle this + visit(path as unknown as any, context); + } else { + builder.addWillMount(path.node); + } + + if (path.isReturnStatement()) { + visitor.ReturnStatement?.(path, context); + break; + } + } +} + +/** + * The process of analyzing the component + * 1. identify the component + * 2. identify the jsx slice in the component + * 2. identify the component's props, including children, alias, and default value + * 3. analyze the early return of the component, build into the branch + * + * @param type + * @param fnName + * @param path + * @param options + */ +export function analyze( + type: CompOrHook, + fnName: string, + path: NodePath, + options: { customAnalyzers?: Analyzer[]; htmlTags: string[] } +) { + const analyzers = options?.customAnalyzers ? options.customAnalyzers : getBuiltinAnalyzers(type); + + const builder = new IRBuilder(fnName, type, path, options.htmlTags); + + analyzeUnitOfWork(fnName, path, { builder, analyzers }); + + return builder.build(); +} + +function getBuiltinAnalyzers(type: CompOrHook) { + if (type === COMPONENT) { + return compBuiltinAnalyzers; + } + if (type === HOOK) { + return hookBuiltinAnalyzers; + } + throw new Error('Unsupported type to analyze'); +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts b/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts index 898731a7afa9fd2f8629a3190c84d8ee0e9034a4..2c143ed4beaefbebd032fb07a2955d11b1a10a9b 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/nodeFactory.ts @@ -1,48 +1,48 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { NodePath } from '@babel/core'; -import { ComponentNode, FunctionalExpression, ReactiveVariable, CompOrHook, HookNode } from './types'; -import { COMPONENT } from '../constants'; - -export function createIRNode( - name: string, - type: T, - fnNode: NodePath, - parent?: ComponentNode -): HookNode | ComponentNode { - const comp: HookNode | ComponentNode = { - type: type === COMPONENT ? 'comp' : 'hook', - params: fnNode.node.params, - level: parent ? parent.level + 1 : 0, - name, - variables: [], - usedBit: 0, - _reactiveBitMap: parent ? new Map(parent._reactiveBitMap) : new Map(), - lifecycle: {}, - parent, - fnNode, - get ownAvailableVariables() { - return [...comp.variables.filter((p): p is ReactiveVariable => p.type === 'reactive')]; - }, - get availableVariables() { - // Here is critical for the dependency analysis, must put parent's availableVariables first - // so the subcomponent can react to the parent's variables change - return [...(comp.parent ? comp.parent.availableVariables : []), ...comp.ownAvailableVariables]; - }, - }; - - return comp; -} +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { NodePath } from '@babel/core'; +import { ComponentNode, FunctionalExpression, ReactiveVariable, CompOrHook, HookNode } from './types'; +import { COMPONENT } from '../constants'; + +export function createIRNode( + name: string, + type: T, + fnNode: NodePath, + parent?: ComponentNode +): HookNode | ComponentNode { + const comp: HookNode | ComponentNode = { + type: type === COMPONENT ? 'comp' : 'hook', + params: fnNode.node.params, + level: parent ? parent.level + 1 : 0, + name, + variables: [], + usedBit: 0, + _reactiveBitMap: parent ? new Map(parent._reactiveBitMap) : new Map(), + lifecycle: {}, + parent, + fnNode, + get ownAvailableVariables() { + return [...comp.variables.filter((p): p is ReactiveVariable => p.type === 'reactive')]; + }, + get availableVariables() { + // Here is critical for the dependency analysis, must put parent's availableVariables first + // so the subcomponent can react to the parent's variables change + return [...(comp.parent ? comp.parent.availableVariables : []), ...comp.ownAvailableVariables]; + }, + }; + + return comp; +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/pruneUnusedState.ts b/packages/transpiler/babel-inula-next-core/src/analyze/pruneUnusedState.ts index bf2e317118b4b393420671787b4ee189a68442e1..2a855e740162ab49b1dd78cd102676e3a281e1b3 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/pruneUnusedState.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/pruneUnusedState.ts @@ -1,159 +1,159 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { ComponentNode, HookNode } from './types'; -import { Bitmap, ViewParticle } from '@openinula/reactivity-parser'; - -/** - * To prune the bitmap of unused properties - * Here are several things to be done in this phase: - * 1. The unused reactive variables will turn to plain variables - * 2. And the bit of the variable should be pruned - * 3. The depMask of the view, computed and watch will be pruned - * etc.: - * ```js - * let a = 1; // 0b001 - * let b = 2; // 0b010 if b is not *used*, so it should be pruned - * let c = 3; // 0b100 -> 0b010(cause bit of b is pruned) - * let d = a + c // The depMask of d should be 0b11, pruned from 0b101 - * ``` - */ -export function pruneUnusedState( - comp: ComponentNode<'comp'> | ComponentNode<'subComp'> | HookNode, - index = 1, - bitPositionToRemoveInParent: number[] = [] -) { - const bitPositionToRemove: number[] = [...bitPositionToRemoveInParent]; - // dfs the component tree - comp.variables = comp.variables.map(v => { - if (v.type === 'reactive') { - // get the index bit, computed should keep the highest bit, etc. 0b0111 -> 0b0100 - const shouldBeReactive = comp.usedBit & keepHighestBit(v.bit); - - if (shouldBeReactive) { - // assign the final bit to the variable, pruning the unused bit - v.bit = 1 << (index++ - bitPositionToRemove.length - 1); - } else { - bitPositionToRemove.push(index++); - v.bit = 0; - } - - // If computed, prune the depMask - if (v.dependency) { - v.dependency.depMask = getDepMask(v.dependency.allDepBits, bitPositionToRemove); - } - } else if (v.type === 'subComp') { - // Recursively prune the subcomponent, keep the index for the next variable - pruneUnusedState(v, index, bitPositionToRemove); - v.usedBit = pruneBitmap(v.usedBit, bitPositionToRemove); - } - - return v; - }); - - comp.watch?.forEach(watch => { - const dependency = watch.dependency; - if (!dependency) { - return; - } - dependency.depMask = getDepMask(dependency.allDepBits, bitPositionToRemove); - }); - - // handle children - if (comp.type === 'hook') { - if (comp.children) { - comp.children.depMask = getDepMask(comp.children.allDepBits, bitPositionToRemove); - } - } else { - if (comp.children) { - comp.children.forEach(child => { - pruneViewParticleUnusedBit(child as ViewParticle, bitPositionToRemove); - }); - } - } -} - -function pruneBitmap(depMask: Bitmap, bitPositionToRemove: number[]) { - // turn the bitmap to binary string - const binaryStr = depMask.toString(2); - const length = binaryStr.length; - // iterate the binaryStr to keep the bit that is not in the bitPositionToRemove - let result = ''; - for (let i = length; i > 0; i--) { - if (!bitPositionToRemove.includes(i)) { - result = result + binaryStr[length - i]; - } - } - - return parseInt(result, 2); -} - -/** - * Get the depMask by pruning the bitPositionToRemove - * The reason why we need to get the depMask from depBitmaps instead of fullDepMask is that - * the fullDepMask contains the bit of used variables, which is not the direct dependency - * - * @param depBitmaps - * @param bitPositionToRemove - */ -function getDepMask(depBitmaps: Bitmap[], bitPositionToRemove: number[]) { - // prune each dependency bitmap and combine them - return depBitmaps.reduce((acc, cur) => { - // computed should keep the highest bit, others should be pruned - return keepHighestBit(pruneBitmap(cur, bitPositionToRemove)) | acc; - }, 0); -} - -function pruneViewParticleUnusedBit(particle: ViewParticle, bitPositionToRemove: number[]) { - // dfs the view particle to prune the bitmap - const doPrune = (value: any) => { - if (value && typeof value === 'object' && 'allDepBits' in value) { - pruneBit(bitPositionToRemove, value); - } - }; - - function traverse(value: any) { - if (value === null || typeof value !== 'object') { - return; - } - - doPrune(value); - - if (Array.isArray(value)) { - value.forEach(traverse); - } else { - for (const key in value) { - traverse(value[key]); - } - } - } - - traverse(particle); -} - -function pruneBit(bitPositionToRemove: number[], prunable: { allDepBits: number[]; depMask?: number }) { - prunable.depMask = getDepMask(prunable.allDepBits, bitPositionToRemove); -} - -function keepHighestBit(bitmap: number) { - // 获取二进制数的长度 - const length = bitmap.toString(2).length; - - // 创建掩码 - const mask = 1 << (length - 1); - - // 使用按位与运算符只保留最高位 - return bitmap & mask; -} +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { ComponentNode, HookNode } from './types'; +import { Bitmap, ViewParticle } from '@openinula/reactivity-parser'; + +/** + * To prune the bitmap of unused properties + * Here are several things to be done in this phase: + * 1. The unused reactive variables will turn to plain variables + * 2. And the bit of the variable should be pruned + * 3. The depMask of the view, computed and watch will be pruned + * etc.: + * ```js + * let a = 1; // 0b001 + * let b = 2; // 0b010 if b is not *used*, so it should be pruned + * let c = 3; // 0b100 -> 0b010(cause bit of b is pruned) + * let d = a + c // The depMask of d should be 0b11, pruned from 0b101 + * ``` + */ +export function pruneUnusedState( + comp: ComponentNode<'comp'> | ComponentNode<'subComp'> | HookNode, + index = 1, + bitPositionToRemoveInParent: number[] = [] +) { + const bitPositionToRemove: number[] = [...bitPositionToRemoveInParent]; + // dfs the component tree + comp.variables = comp.variables.map(v => { + if (v.type === 'reactive') { + // get the index bit, computed should keep the highest bit, etc. 0b0111 -> 0b0100 + const shouldBeReactive = comp.usedBit & keepHighestBit(v.bit); + + if (shouldBeReactive) { + // assign the final bit to the variable, pruning the unused bit + v.bit = 1 << (index++ - bitPositionToRemove.length - 1); + } else { + bitPositionToRemove.push(index++); + v.bit = 0; + } + + // If computed, prune the depMask + if (v.dependency) { + v.dependency.depMask = getDepMask(v.dependency.allDepBits, bitPositionToRemove); + } + } else if (v.type === 'subComp') { + // Recursively prune the subcomponent, keep the index for the next variable + pruneUnusedState(v, index, bitPositionToRemove); + v.usedBit = pruneBitmap(v.usedBit, bitPositionToRemove); + } + + return v; + }); + + comp.watch?.forEach(watch => { + const dependency = watch.dependency; + if (!dependency) { + return; + } + dependency.depMask = getDepMask(dependency.allDepBits, bitPositionToRemove); + }); + + // handle children + if (comp.type === 'hook') { + if (comp.children) { + comp.children.depMask = getDepMask(comp.children.allDepBits, bitPositionToRemove); + } + } else { + if (comp.children) { + comp.children.forEach(child => { + pruneViewParticleUnusedBit(child as ViewParticle, bitPositionToRemove); + }); + } + } +} + +function pruneBitmap(depMask: Bitmap, bitPositionToRemove: number[]) { + // turn the bitmap to binary string + const binaryStr = depMask.toString(2); + const length = binaryStr.length; + // iterate the binaryStr to keep the bit that is not in the bitPositionToRemove + let result = ''; + for (let i = length; i > 0; i--) { + if (!bitPositionToRemove.includes(i)) { + result = result + binaryStr[length - i]; + } + } + + return parseInt(result, 2); +} + +/** + * Get the depMask by pruning the bitPositionToRemove + * The reason why we need to get the depMask from depBitmaps instead of fullDepMask is that + * the fullDepMask contains the bit of used variables, which is not the direct dependency + * + * @param depBitmaps + * @param bitPositionToRemove + */ +function getDepMask(depBitmaps: Bitmap[], bitPositionToRemove: number[]) { + // prune each dependency bitmap and combine them + return depBitmaps.reduce((acc, cur) => { + // computed should keep the highest bit, others should be pruned + return keepHighestBit(pruneBitmap(cur, bitPositionToRemove)) | acc; + }, 0); +} + +function pruneViewParticleUnusedBit(particle: ViewParticle, bitPositionToRemove: number[]) { + // dfs the view particle to prune the bitmap + const doPrune = (value: any) => { + if (value && typeof value === 'object' && 'allDepBits' in value) { + pruneBit(bitPositionToRemove, value); + } + }; + + function traverse(value: any) { + if (value === null || typeof value !== 'object') { + return; + } + + doPrune(value); + + if (Array.isArray(value)) { + value.forEach(traverse); + } else { + for (const key in value) { + traverse(value[key]); + } + } + } + + traverse(particle); +} + +function pruneBit(bitPositionToRemove: number[], prunable: { allDepBits: number[]; depMask?: number }) { + prunable.depMask = getDepMask(prunable.allDepBits, bitPositionToRemove); +} + +function keepHighestBit(bitmap: number) { + // 获取二进制数的长度 + const length = bitmap.toString(2).length; + + // 创建掩码 + const mask = 1 << (length - 1); + + // 使用按位与运算符只保留最高位 + return bitmap & mask; +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/types.ts b/packages/transpiler/babel-inula-next-core/src/analyze/types.ts index bf6e82093d54f34345dbd1b1ce04a811b04d55ba..d40f186b5fa33aef36017ca36479be1b4e9032e4 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/types.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/types.ts @@ -1,137 +1,137 @@ -/* - * 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 NodePath, types as t } from '@babel/core'; -import { COMPONENT, DID_MOUNT, DID_UNMOUNT, HOOK, PropType, WILL_MOUNT, WILL_UNMOUNT } from '../constants'; -import { Bitmap, ViewParticle } from '@openinula/reactivity-parser'; -import { IRBuilder } from './IRBuilder'; - -export type CompOrHook = typeof COMPONENT | typeof HOOK; -export type LifeCycle = typeof WILL_MOUNT | typeof DID_MOUNT | typeof WILL_UNMOUNT | typeof DID_UNMOUNT; -export type Dependency = { - dependenciesNode: t.ArrayExpression; - /** - * Only contains the bit of direct dependencies and not contains the bit of used variables - * So it's configured in pruneUnusedBit.ts - */ - depMask?: Bitmap; - /** - * The bitmap of each dependency - */ - allDepBits: Bitmap[]; -}; -export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression; - -export interface BaseVariable { - name: string; - value: V; - kind: t.VariableDeclaration['kind']; -} - -export interface ReactiveVariable extends BaseVariable { - type: 'reactive'; - level: number; - /** - * The bitmap of the variable that should be used in the codegen - */ - bit: Bitmap; - dependency: Dependency | null; -} - -export type SubCompVariable = ComponentNode<'subComp'>; - -// Including static variable and method -export interface PlainVariable { - type: 'plain'; - value: t.Statement; -} - -export type Variable = ReactiveVariable | PlainVariable | SubCompVariable; - -export type WatchFunc = { - dependency: Dependency | null; - callback: NodePath | NodePath; -}; - -export interface Prop { - name: string; - type: PropType; - alias: string | null; - default: t.Expression | null; - nestedProps: string[] | null; - nestedRelationship: t.ObjectPattern | t.ArrayPattern | null; -} - -export interface IRNode { - name: string; - level: number; - params: t.FunctionExpression['params']; - // The variables defined in the component - variables: Variable[]; - usedBit: Bitmap; - /** - * The map to find the reactive bitmap by name - */ - _reactiveBitMap: Map; - /** - * The available variables and props owned by the component - */ - ownAvailableVariables: ReactiveVariable[]; - /** - * The available variables and props for the component and its parent - */ - availableVariables: ReactiveVariable[]; - parent?: ComponentNode; - /** - * The function body of the fn component code - */ - fnNode: NodePath; - lifecycle: Partial>; - /** - * The watch fn in the component - */ - watch?: WatchFunc[]; -} - -export interface ComponentNode extends IRNode { - type: Type; - children?: ViewParticle[]; -} -export type SubComponentNode = SubCompVariable; -export interface HookNode extends IRNode { - type: 'hook'; - children?: { - value: t.Expression; - depMask?: number; // -> bit - allDepBits: number[]; - dependenciesNode: t.ArrayExpression; - }; -} - -export interface AnalyzeContext { - builder: IRBuilder; - analyzers: Analyzer[]; -} - -export type Visitor = { - [Type in t.Statement['type']]?: (path: NodePath>, state: S) => void; -} & { - Prop?: (path: NodePath, state: S) => void; -}; -export type Analyzer = () => Visitor; - -export interface FnComponentDeclaration extends t.FunctionDeclaration { - id: t.Identifier; -} +/* + * 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 NodePath, types as t } from '@babel/core'; +import { COMPONENT, DID_MOUNT, DID_UNMOUNT, HOOK, PropType, WILL_MOUNT, WILL_UNMOUNT } from '../constants'; +import { Bitmap, ViewParticle } from '@openinula/reactivity-parser'; +import { IRBuilder } from './IRBuilder'; + +export type CompOrHook = typeof COMPONENT | typeof HOOK; +export type LifeCycle = typeof WILL_MOUNT | typeof DID_MOUNT | typeof WILL_UNMOUNT | typeof DID_UNMOUNT; +export type Dependency = { + dependenciesNode: t.ArrayExpression; + /** + * Only contains the bit of direct dependencies and not contains the bit of used variables + * So it's configured in pruneUnusedBit.ts + */ + depMask?: Bitmap; + /** + * The bitmap of each dependency + */ + allDepBits: Bitmap[]; +}; +export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression; + +export interface BaseVariable { + name: string; + value: V; + kind: t.VariableDeclaration['kind']; +} + +export interface ReactiveVariable extends BaseVariable { + type: 'reactive'; + level: number; + /** + * The bitmap of the variable that should be used in the codegen + */ + bit: Bitmap; + dependency: Dependency | null; +} + +export type SubCompVariable = ComponentNode<'subComp'>; + +// Including static variable and method +export interface PlainVariable { + type: 'plain'; + value: t.Statement; +} + +export type Variable = ReactiveVariable | PlainVariable | SubCompVariable; + +export type WatchFunc = { + dependency: Dependency | null; + callback: NodePath | NodePath; +}; + +export interface Prop { + name: string; + type: PropType; + alias: string | null; + default: t.Expression | null; + nestedProps: string[] | null; + nestedRelationship: t.ObjectPattern | t.ArrayPattern | null; +} + +export interface IRNode { + name: string; + level: number; + params: t.FunctionExpression['params']; + // The variables defined in the component + variables: Variable[]; + usedBit: Bitmap; + /** + * The map to find the reactive bitmap by name + */ + _reactiveBitMap: Map; + /** + * The available variables and props owned by the component + */ + ownAvailableVariables: ReactiveVariable[]; + /** + * The available variables and props for the component and its parent + */ + availableVariables: ReactiveVariable[]; + parent?: ComponentNode; + /** + * The function body of the fn component code + */ + fnNode: NodePath; + lifecycle: Partial>; + /** + * The watch fn in the component + */ + watch?: WatchFunc[]; +} + +export interface ComponentNode extends IRNode { + type: Type; + children?: ViewParticle[]; +} +export type SubComponentNode = SubCompVariable; +export interface HookNode extends IRNode { + type: 'hook'; + children?: { + value: t.Expression; + depMask?: number; // -> bit + allDepBits: number[]; + dependenciesNode: t.ArrayExpression; + }; +} + +export interface AnalyzeContext { + builder: IRBuilder; + analyzers: Analyzer[]; +} + +export type Visitor = { + [Type in t.Statement['type']]?: (path: NodePath>, state: S) => void; +} & { + Prop?: (path: NodePath, state: S) => void; +}; +export type Analyzer = () => Visitor; + +export interface FnComponentDeclaration extends t.FunctionDeclaration { + id: t.Identifier; +} diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/utils.ts b/packages/transpiler/babel-inula-next-core/src/analyze/utils.ts index 2918694cf701c534a80d379c0b033ad475d74fd9..02e9ae0d0f941fc48fda0a31677fbab1924fef4e 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/utils.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/utils.ts @@ -1,78 +1,78 @@ -/* - * 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 { types as t } from '@openinula/babel-api'; -import { NodePath } from '@babel/core'; -import { ComponentNode, FnComponentDeclaration, HookNode } from './types'; - -export function isValidPath(path: NodePath): path is NodePath> { - return !!path.node; -} - -// The component name must be UpperCamelCase -export function isValidComponent(node: t.FunctionDeclaration): node is FnComponentDeclaration { - // the first letter of the component name must be uppercase - return node.id ? isValidComponentName(node.id.name) : false; -} - -export function isValidComponentName(name: string) { - // the first letter of the component name must be uppercase - return /^[A-Z]/.test(name); -} - -export function hasJSX(path: NodePath) { - if (path.isJSXElement()) { - return true; - } - - // check if there is JSXElement in the children - let seen = false; - path.traverse({ - JSXElement() { - seen = true; - }, - }); - return seen; -} - -export function extractFnBody(node: t.FunctionExpression | t.ArrowFunctionExpression): t.Statement { - if (node.async) { - // async should return iife - return t.expressionStatement(t.callExpression(node, [])); - } - - // For non-async functions, just return the body - return t.isStatement(node.body) ? node.body : t.expressionStatement(node.body); -} - -export function isStaticValue(node: t.VariableDeclarator['init']) { - return ( - (t.isLiteral(node) && !t.isTemplateLiteral(node)) || - t.isArrowFunctionExpression(node) || - t.isFunctionExpression(node) - ); -} - -export function assertComponentNode(node: any): asserts node is ComponentNode { - if (node.type !== 'comp' && node.type !== 'subComp') { - throw new Error('Analyze: Should be component node'); - } -} - -export function assertHookNode(node: any): asserts node is HookNode { - if (node.type !== 'hook') { - throw new Error('Analyze: Should be hook node'); - } -} +/* + * 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 { types as t } from '@openinula/babel-api'; +import { NodePath } from '@babel/core'; +import { ComponentNode, FnComponentDeclaration, HookNode } from './types'; + +export function isValidPath(path: NodePath): path is NodePath> { + return !!path.node; +} + +// The component name must be UpperCamelCase +export function isValidComponent(node: t.FunctionDeclaration): node is FnComponentDeclaration { + // the first letter of the component name must be uppercase + return node.id ? isValidComponentName(node.id.name) : false; +} + +export function isValidComponentName(name: string) { + // the first letter of the component name must be uppercase + return /^[A-Z]/.test(name); +} + +export function hasJSX(path: NodePath) { + if (path.isJSXElement()) { + return true; + } + + // check if there is JSXElement in the children + let seen = false; + path.traverse({ + JSXElement() { + seen = true; + }, + }); + return seen; +} + +export function extractFnBody(node: t.FunctionExpression | t.ArrowFunctionExpression): t.Statement { + if (node.async) { + // async should return iife + return t.expressionStatement(t.callExpression(node, [])); + } + + // For non-async functions, just return the body + return t.isStatement(node.body) ? node.body : t.expressionStatement(node.body); +} + +export function isStaticValue(node: t.VariableDeclarator['init']) { + return ( + (t.isLiteral(node) && !t.isTemplateLiteral(node)) || + t.isArrowFunctionExpression(node) || + t.isFunctionExpression(node) + ); +} + +export function assertComponentNode(node: any): asserts node is ComponentNode { + if (node.type !== 'comp' && node.type !== 'subComp') { + throw new Error('Analyze: Should be component node'); + } +} + +export function assertHookNode(node: any): asserts node is HookNode { + if (node.type !== 'hook') { + throw new Error('Analyze: Should be hook node'); + } +} diff --git a/packages/transpiler/babel-inula-next-core/src/constants.ts b/packages/transpiler/babel-inula-next-core/src/constants.ts index 20637a4c9423eb0aadd8b84b520333043ba021d1..7bcaa98849e17ca2adfad27c882ed792aa26bd99 100644 --- a/packages/transpiler/babel-inula-next-core/src/constants.ts +++ b/packages/transpiler/babel-inula-next-core/src/constants.ts @@ -1,517 +1,519 @@ -export const COMPONENT = 'Component'; -export const HOOK = 'Hook'; -export const WILL_MOUNT = 'willMount'; -export const DID_MOUNT = 'didMount'; -export const WILL_UNMOUNT = 'willUnmount'; -export const DID_UNMOUNT = 'didUnmount'; - -export const WATCH = 'watch'; - -export enum PropType { - REST = 'rest', - SINGLE = 'single', -} - -export const reactivityFuncNames = [ - // ---- Array - 'push', - 'pop', - 'shift', - 'unshift', - 'splice', - 'sort', - 'reverse', - // ---- Set - 'add', - 'delete', - 'clear', - // ---- Map - 'set', - 'delete', - 'clear', -]; - -export const originalImportMap = Object.fromEntries( - [ - 'createElement', - 'createComponent', - 'setStyle', - 'setDataset', - 'setEvent', - 'delegateEvent', - 'setHTMLProp', - 'setHTMLAttr', - 'setHTMLProps', - 'setHTMLAttrs', - 'createTextNode', - 'updateText', - 'insertNode', - 'appendNode', - 'ForNode', - 'CondNode', - 'ExpNode', - 'ContextProvider', - 'PropView', - 'render', - 'notCached', - 'Comp', - 'useHook', - 'createHook', - 'untrack', - 'runOnce', - ].map(name => [name, `$$${name}`]) -); - -const accessedKeys = new Set(); - -export const importMap = new Proxy(originalImportMap, { - get(target, prop: string, receiver) { - accessedKeys.add(prop); - return Reflect.get(target, prop, receiver); - }, -}); - -// 函数用于获取被访问过的键 -export function getAccessedKeys() { - return Array.from(accessedKeys).reduce>((map, key) => { - map[key] = originalImportMap[key]; - return map; - }, {}); -} - -// 函数用于重置访问记录 -export function resetAccessedKeys() { - accessedKeys.clear(); -} - -export const alterAttributeMap = { - class: 'className', - for: 'htmlFor', -}; - -/** - * @brief HTML internal attribute map, can be accessed as js property - */ -export const defaultAttributeMap = { - // ---- Other property as attribute - textContent: ['*'], - innerHTML: ['*'], - // ---- Source: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes - accept: ['form', 'input'], - // ---- Original: accept-charset - acceptCharset: ['form'], - accesskey: ['*'], - action: ['form'], - align: ['caption', 'col', 'colgroup', 'hr', 'iframe', 'img', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr'], - allow: ['iframe'], - alt: ['area', 'img', 'input'], - async: ['script'], - autocapitalize: ['*'], - autocomplete: ['form', 'input', 'select', 'textarea'], - autofocus: ['button', 'input', 'select', 'textarea'], - autoplay: ['audio', 'video'], - background: ['body', 'table', 'td', 'th'], - // ---- Original: base - bgColor: ['body', 'col', 'colgroup', 'marquee', 'table', 'tbody', 'tfoot', 'td', 'th', 'tr'], - border: ['img', 'object', 'table'], - buffered: ['audio', 'video'], - capture: ['input'], - charset: ['meta'], - checked: ['input'], - cite: ['blockquote', 'del', 'ins', 'q'], - className: ['*'], - color: ['font', 'hr'], - cols: ['textarea'], - // ---- Original: colspan - colSpan: ['td', 'th'], - content: ['meta'], - // ---- Original: contenteditable - contentEditable: ['*'], - contextmenu: ['*'], - controls: ['audio', 'video'], - coords: ['area'], - crossOrigin: ['audio', 'img', 'link', 'script', 'video'], - csp: ['iframe'], - data: ['object'], - // ---- Original: datetime - dateTime: ['del', 'ins', 'time'], - decoding: ['img'], - default: ['track'], - defer: ['script'], - dir: ['*'], - dirname: ['input', 'textarea'], - disabled: ['button', 'fieldset', 'input', 'optgroup', 'option', 'select', 'textarea'], - download: ['a', 'area'], - draggable: ['*'], - enctype: ['form'], - // ---- Original: enterkeyhint - enterKeyHint: ['textarea', 'contenteditable'], - htmlFor: ['label', 'output'], - form: ['button', 'fieldset', 'input', 'label', 'meter', 'object', 'output', 'progress', 'select', 'textarea'], - // ---- Original: formaction - formAction: ['input', 'button'], - // ---- Original: formenctype - formEnctype: ['button', 'input'], - // ---- Original: formmethod - formMethod: ['button', 'input'], - // ---- Original: formnovalidate - formNoValidate: ['button', 'input'], - // ---- Original: formtarget - formTarget: ['button', 'input'], - headers: ['td', 'th'], - height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'], - hidden: ['*'], - high: ['meter'], - href: ['a', 'area', 'base', 'link'], - hreflang: ['a', 'link'], - // ---- Original: http-equiv - httpEquiv: ['meta'], - id: ['*'], - integrity: ['link', 'script'], - // ---- Original: intrinsicsize - intrinsicSize: ['img'], - // ---- Original: inputmode - inputMode: ['textarea', 'contenteditable'], - ismap: ['img'], - // ---- Original: itemprop - itemProp: ['*'], - kind: ['track'], - label: ['optgroup', 'option', 'track'], - lang: ['*'], - language: ['script'], - loading: ['img', 'iframe'], - list: ['input'], - loop: ['audio', 'marquee', 'video'], - low: ['meter'], - manifest: ['html'], - max: ['input', 'meter', 'progress'], - // ---- Original: maxlength - maxLength: ['input', 'textarea'], - // ---- Original: minlength - minLength: ['input', 'textarea'], - media: ['a', 'area', 'link', 'source', 'style'], - method: ['form'], - min: ['input', 'meter'], - multiple: ['input', 'select'], - muted: ['audio', 'video'], - name: [ - 'button', - 'form', - 'fieldset', - 'iframe', - 'input', - 'object', - 'output', - 'select', - 'textarea', - 'map', - 'meta', - 'param', - ], - // ---- Original: novalidate - noValidate: ['form'], - open: ['details', 'dialog'], - optimum: ['meter'], - pattern: ['input'], - ping: ['a', 'area'], - placeholder: ['input', 'textarea'], - // ---- Original: playsinline - playsInline: ['video'], - poster: ['video'], - preload: ['audio', 'video'], - readonly: ['input', 'textarea'], - // ---- Original: referrerpolicy - referrerPolicy: ['a', 'area', 'iframe', 'img', 'link', 'script'], - rel: ['a', 'area', 'link'], - required: ['input', 'select', 'textarea'], - reversed: ['ol'], - role: ['*'], - rows: ['textarea'], - // ---- Original: rowspan - rowSpan: ['td', 'th'], - sandbox: ['iframe'], - scope: ['th'], - scoped: ['style'], - selected: ['option'], - shape: ['a', 'area'], - size: ['input', 'select'], - sizes: ['link', 'img', 'source'], - slot: ['*'], - span: ['col', 'colgroup'], - spellcheck: ['*'], - src: ['audio', 'embed', 'iframe', 'img', 'input', 'script', 'source', 'track', 'video'], - srcdoc: ['iframe'], - srclang: ['track'], - srcset: ['img', 'source'], - start: ['ol'], - step: ['input'], - style: ['*'], - summary: ['table'], - // ---- Original: tabindex - tabIndex: ['*'], - target: ['a', 'area', 'base', 'form'], - title: ['*'], - translate: ['*'], - type: ['button', 'input', 'embed', 'object', 'ol', 'script', 'source', 'style', 'menu', 'link'], - usemap: ['img', 'input', 'object'], - value: ['button', 'data', 'input', 'li', 'meter', 'option', 'progress', 'param', 'text' /** extra for TextNode */], - width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'], - wrap: ['textarea'], - // --- ARIA attributes - // Source: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes - ariaAutocomplete: ['*'], - ariaChecked: ['*'], - ariaDisabled: ['*'], - ariaErrorMessage: ['*'], - ariaExpanded: ['*'], - ariaHasPopup: ['*'], - ariaHidden: ['*'], - ariaInvalid: ['*'], - ariaLabel: ['*'], - ariaLevel: ['*'], - ariaModal: ['*'], - ariaMultiline: ['*'], - ariaMultiSelectable: ['*'], - ariaOrientation: ['*'], - ariaPlaceholder: ['*'], - ariaPressed: ['*'], - ariaReadonly: ['*'], - ariaRequired: ['*'], - ariaSelected: ['*'], - ariaSort: ['*'], - ariaValuemax: ['*'], - ariaValuemin: ['*'], - ariaValueNow: ['*'], - ariaValueText: ['*'], - ariaBusy: ['*'], - ariaLive: ['*'], - ariaRelevant: ['*'], - ariaAtomic: ['*'], - ariaDropEffect: ['*'], - ariaGrabbed: ['*'], - ariaActiveDescendant: ['*'], - ariaColCount: ['*'], - ariaColIndex: ['*'], - ariaColSpan: ['*'], - ariaControls: ['*'], - ariaDescribedBy: ['*'], - ariaDescription: ['*'], - ariaDetails: ['*'], - ariaFlowTo: ['*'], - ariaLabelledBy: ['*'], - ariaOwns: ['*'], - ariaPosInset: ['*'], - ariaRowCount: ['*'], - ariaRowIndex: ['*'], - ariaRowSpan: ['*'], - ariaSetSize: ['*'], -}; - -export const defaultHTMLTags = [ - 'a', - 'abbr', - 'address', - 'area', - 'article', - 'aside', - 'audio', - 'b', - 'base', - 'bdi', - 'bdo', - 'blockquote', - 'body', - 'br', - 'button', - 'canvas', - 'caption', - 'cite', - 'code', - 'col', - 'colgroup', - 'data', - 'datalist', - 'dd', - 'del', - 'details', - 'dfn', - 'dialog', - 'div', - 'dl', - 'dt', - 'em', - 'embed', - 'fieldset', - 'figcaption', - 'figure', - 'footer', - 'form', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'head', - 'header', - 'hgroup', - 'hr', - 'html', - 'i', - 'iframe', - 'img', - 'input', - 'ins', - 'kbd', - 'label', - 'legend', - 'li', - 'link', - 'main', - 'map', - 'mark', - 'menu', - 'meta', - 'meter', - 'nav', - 'noscript', - 'object', - 'ol', - 'optgroup', - 'option', - 'output', - 'p', - 'picture', - 'pre', - 'progress', - 'q', - 'rp', - 'rt', - 'ruby', - 's', - 'samp', - 'script', - 'section', - 'select', - 'slot', - 'small', - 'source', - 'span', - 'strong', - 'style', - 'sub', - 'summary', - 'sup', - 'table', - 'tbody', - 'td', - 'template', - 'textarea', - 'tfoot', - 'th', - 'thead', - 'time', - 'title', - 'tr', - 'track', - 'u', - 'ul', - 'var', - 'video', - 'wbr', - 'acronym', - 'applet', - 'basefont', - 'bgsound', - 'big', - 'blink', - 'center', - 'dir', - 'font', - 'frame', - 'frameset', - 'isindex', - 'keygen', - 'listing', - 'marquee', - 'menuitem', - 'multicol', - 'nextid', - 'nobr', - 'noembed', - 'noframes', - 'param', - 'plaintext', - 'rb', - 'rtc', - 'spacer', - 'strike', - 'tt', - 'xmp', - 'animate', - 'animateMotion', - 'animateTransform', - 'circle', - 'clipPath', - 'defs', - 'desc', - 'ellipse', - 'feBlend', - 'feColorMatrix', - 'feComponentTransfer', - 'feComposite', - 'feConvolveMatrix', - 'feDiffuseLighting', - 'feDisplacementMap', - 'feDistantLight', - 'feDropShadow', - 'feFlood', - 'feFuncA', - 'feFuncB', - 'feFuncG', - 'feFuncR', - 'feGaussianBlur', - 'feImage', - 'feMerge', - 'feMergeNode', - 'feMorphology', - 'feOffset', - 'fePointLight', - 'feSpecularLighting', - 'feSpotLight', - 'feTile', - 'feTurbulence', - 'filter', - 'foreignObject', - 'g', - 'image', - 'line', - 'linearGradient', - 'marker', - 'mask', - 'metadata', - 'mpath', - 'path', - 'pattern', - 'polygon', - 'polyline', - 'radialGradient', - 'rect', - 'set', - 'stop', - 'svg', - 'switch', - 'symbol', - 'text', - 'textPath', - 'tspan', - 'use', - 'view', -]; -export const PROP_SUFFIX = '_$p$_'; -export const HOOK_SUFFIX = '_$h$_'; -// The suffix means to bind to context specific key -export const SPECIFIC_CTX_SUFFIX = '_$c$_'; -// The suffix means to bind to whole context -export const WHOLE_CTX_SUFFIX = '_$ctx$_'; -export const builtinHooks = ['useContext']; +export const COMPONENT = 'Component'; +export const HOOK = 'Hook'; +export const WILL_MOUNT = 'willMount'; +export const DID_MOUNT = 'didMount'; +export const WILL_UNMOUNT = 'willUnmount'; +export const DID_UNMOUNT = 'didUnmount'; + +export const WATCH = 'watch'; + +export enum PropType { + REST = 'rest', + SINGLE = 'single', +} + +export const reactivityFuncNames = [ + // ---- Array + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse', + // ---- Set + 'add', + 'delete', + 'clear', + // ---- Map + 'set', + 'delete', + 'clear', +]; + +export const originalImportMap = Object.fromEntries( + [ + 'createElement', + 'createComponent', + 'setStyle', + 'setDataset', + 'setEvent', + 'delegateEvent', + 'setHTMLProp', + 'setHTMLAttr', + 'setHTMLProps', + 'setHTMLAttrs', + 'createTextNode', + 'updateText', + 'insertNode', + 'appendNode', + 'render', + 'notCached', + 'Comp', + 'useHook', + 'createHook', + 'untrack', + 'runOnce', + 'createNode', + 'updateNode', + 'updateChildren', + 'setProp', + 'initContextChildren', + 'initCompNode', + 'emitUpdate', + ].map(name => [name, `$$${name}`]) +); + +const accessedKeys = new Set(); + +export const importMap = new Proxy(originalImportMap, { + get(target, prop: string, receiver) { + accessedKeys.add(prop); + return Reflect.get(target, prop, receiver); + }, +}); + +// 函数用于获取被访问过的键 +export function getAccessedKeys() { + return Array.from(accessedKeys).reduce>((map, key) => { + map[key] = originalImportMap[key]; + return map; + }, {}); +} + +// 函数用于重置访问记录 +export function resetAccessedKeys() { + accessedKeys.clear(); +} + +export const alterAttributeMap = { + class: 'className', + for: 'htmlFor', +}; + +/** + * @brief HTML internal attribute map, can be accessed as js property + */ +export const defaultAttributeMap = { + // ---- Other property as attribute + textContent: ['*'], + innerHTML: ['*'], + // ---- Source: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes + accept: ['form', 'input'], + // ---- Original: accept-charset + acceptCharset: ['form'], + accesskey: ['*'], + action: ['form'], + align: ['caption', 'col', 'colgroup', 'hr', 'iframe', 'img', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr'], + allow: ['iframe'], + alt: ['area', 'img', 'input'], + async: ['script'], + autocapitalize: ['*'], + autocomplete: ['form', 'input', 'select', 'textarea'], + autofocus: ['button', 'input', 'select', 'textarea'], + autoplay: ['audio', 'video'], + background: ['body', 'table', 'td', 'th'], + // ---- Original: base + bgColor: ['body', 'col', 'colgroup', 'marquee', 'table', 'tbody', 'tfoot', 'td', 'th', 'tr'], + border: ['img', 'object', 'table'], + buffered: ['audio', 'video'], + capture: ['input'], + charset: ['meta'], + checked: ['input'], + cite: ['blockquote', 'del', 'ins', 'q'], + className: ['*'], + color: ['font', 'hr'], + cols: ['textarea'], + // ---- Original: colspan + colSpan: ['td', 'th'], + content: ['meta'], + // ---- Original: contenteditable + contentEditable: ['*'], + contextmenu: ['*'], + controls: ['audio', 'video'], + coords: ['area'], + crossOrigin: ['audio', 'img', 'link', 'script', 'video'], + csp: ['iframe'], + data: ['object'], + // ---- Original: datetime + dateTime: ['del', 'ins', 'time'], + decoding: ['img'], + default: ['track'], + defer: ['script'], + dir: ['*'], + dirname: ['input', 'textarea'], + disabled: ['button', 'fieldset', 'input', 'optgroup', 'option', 'select', 'textarea'], + download: ['a', 'area'], + draggable: ['*'], + enctype: ['form'], + // ---- Original: enterkeyhint + enterKeyHint: ['textarea', 'contenteditable'], + htmlFor: ['label', 'output'], + form: ['button', 'fieldset', 'input', 'label', 'meter', 'object', 'output', 'progress', 'select', 'textarea'], + // ---- Original: formaction + formAction: ['input', 'button'], + // ---- Original: formenctype + formEnctype: ['button', 'input'], + // ---- Original: formmethod + formMethod: ['button', 'input'], + // ---- Original: formnovalidate + formNoValidate: ['button', 'input'], + // ---- Original: formtarget + formTarget: ['button', 'input'], + headers: ['td', 'th'], + height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'], + hidden: ['*'], + high: ['meter'], + href: ['a', 'area', 'base', 'link'], + hreflang: ['a', 'link'], + // ---- Original: http-equiv + httpEquiv: ['meta'], + id: ['*'], + integrity: ['link', 'script'], + // ---- Original: intrinsicsize + intrinsicSize: ['img'], + // ---- Original: inputmode + inputMode: ['textarea', 'contenteditable'], + ismap: ['img'], + // ---- Original: itemprop + itemProp: ['*'], + kind: ['track'], + label: ['optgroup', 'option', 'track'], + lang: ['*'], + language: ['script'], + loading: ['img', 'iframe'], + list: ['input'], + loop: ['audio', 'marquee', 'video'], + low: ['meter'], + manifest: ['html'], + max: ['input', 'meter', 'progress'], + // ---- Original: maxlength + maxLength: ['input', 'textarea'], + // ---- Original: minlength + minLength: ['input', 'textarea'], + media: ['a', 'area', 'link', 'source', 'style'], + method: ['form'], + min: ['input', 'meter'], + multiple: ['input', 'select'], + muted: ['audio', 'video'], + name: [ + 'button', + 'form', + 'fieldset', + 'iframe', + 'input', + 'object', + 'output', + 'select', + 'textarea', + 'map', + 'meta', + 'param', + ], + // ---- Original: novalidate + noValidate: ['form'], + open: ['details', 'dialog'], + optimum: ['meter'], + pattern: ['input'], + ping: ['a', 'area'], + placeholder: ['input', 'textarea'], + // ---- Original: playsinline + playsInline: ['video'], + poster: ['video'], + preload: ['audio', 'video'], + readonly: ['input', 'textarea'], + // ---- Original: referrerpolicy + referrerPolicy: ['a', 'area', 'iframe', 'img', 'link', 'script'], + rel: ['a', 'area', 'link'], + required: ['input', 'select', 'textarea'], + reversed: ['ol'], + role: ['*'], + rows: ['textarea'], + // ---- Original: rowspan + rowSpan: ['td', 'th'], + sandbox: ['iframe'], + scope: ['th'], + scoped: ['style'], + selected: ['option'], + shape: ['a', 'area'], + size: ['input', 'select'], + sizes: ['link', 'img', 'source'], + slot: ['*'], + span: ['col', 'colgroup'], + spellcheck: ['*'], + src: ['audio', 'embed', 'iframe', 'img', 'input', 'script', 'source', 'track', 'video'], + srcdoc: ['iframe'], + srclang: ['track'], + srcset: ['img', 'source'], + start: ['ol'], + step: ['input'], + style: ['*'], + summary: ['table'], + // ---- Original: tabindex + tabIndex: ['*'], + target: ['a', 'area', 'base', 'form'], + title: ['*'], + translate: ['*'], + type: ['button', 'input', 'embed', 'object', 'ol', 'script', 'source', 'style', 'menu', 'link'], + usemap: ['img', 'input', 'object'], + value: ['button', 'data', 'input', 'li', 'meter', 'option', 'progress', 'param', 'text' /** extra for TextNode */], + width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'], + wrap: ['textarea'], + // --- ARIA attributes + // Source: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes + ariaAutocomplete: ['*'], + ariaChecked: ['*'], + ariaDisabled: ['*'], + ariaErrorMessage: ['*'], + ariaExpanded: ['*'], + ariaHasPopup: ['*'], + ariaHidden: ['*'], + ariaInvalid: ['*'], + ariaLabel: ['*'], + ariaLevel: ['*'], + ariaModal: ['*'], + ariaMultiline: ['*'], + ariaMultiSelectable: ['*'], + ariaOrientation: ['*'], + ariaPlaceholder: ['*'], + ariaPressed: ['*'], + ariaReadonly: ['*'], + ariaRequired: ['*'], + ariaSelected: ['*'], + ariaSort: ['*'], + ariaValuemax: ['*'], + ariaValuemin: ['*'], + ariaValueNow: ['*'], + ariaValueText: ['*'], + ariaBusy: ['*'], + ariaLive: ['*'], + ariaRelevant: ['*'], + ariaAtomic: ['*'], + ariaDropEffect: ['*'], + ariaGrabbed: ['*'], + ariaActiveDescendant: ['*'], + ariaColCount: ['*'], + ariaColIndex: ['*'], + ariaColSpan: ['*'], + ariaControls: ['*'], + ariaDescribedBy: ['*'], + ariaDescription: ['*'], + ariaDetails: ['*'], + ariaFlowTo: ['*'], + ariaLabelledBy: ['*'], + ariaOwns: ['*'], + ariaPosInset: ['*'], + ariaRowCount: ['*'], + ariaRowIndex: ['*'], + ariaRowSpan: ['*'], + ariaSetSize: ['*'], +}; + +export const defaultHTMLTags = [ + 'a', + 'abbr', + 'address', + 'area', + 'article', + 'aside', + 'audio', + 'b', + 'base', + 'bdi', + 'bdo', + 'blockquote', + 'body', + 'br', + 'button', + 'canvas', + 'caption', + 'cite', + 'code', + 'col', + 'colgroup', + 'data', + 'datalist', + 'dd', + 'del', + 'details', + 'dfn', + 'dialog', + 'div', + 'dl', + 'dt', + 'em', + 'embed', + 'fieldset', + 'figcaption', + 'figure', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'header', + 'hgroup', + 'hr', + 'html', + 'i', + 'iframe', + 'img', + 'input', + 'ins', + 'kbd', + 'label', + 'legend', + 'li', + 'link', + 'main', + 'map', + 'mark', + 'menu', + 'meta', + 'meter', + 'nav', + 'noscript', + 'object', + 'ol', + 'optgroup', + 'option', + 'output', + 'p', + 'picture', + 'pre', + 'progress', + 'q', + 'rp', + 'rt', + 'ruby', + 's', + 'samp', + 'script', + 'section', + 'select', + 'slot', + 'small', + 'source', + 'span', + 'strong', + 'style', + 'sub', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'template', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'time', + 'title', + 'tr', + 'track', + 'u', + 'ul', + 'var', + 'video', + 'wbr', + 'acronym', + 'applet', + 'basefont', + 'bgsound', + 'big', + 'blink', + 'center', + 'dir', + 'font', + 'frame', + 'frameset', + 'isindex', + 'keygen', + 'listing', + 'marquee', + 'menuitem', + 'multicol', + 'nextid', + 'nobr', + 'noembed', + 'noframes', + 'param', + 'plaintext', + 'rb', + 'rtc', + 'spacer', + 'strike', + 'tt', + 'xmp', + 'animate', + 'animateMotion', + 'animateTransform', + 'circle', + 'clipPath', + 'defs', + 'desc', + 'ellipse', + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', + 'filter', + 'foreignObject', + 'g', + 'image', + 'line', + 'linearGradient', + 'marker', + 'mask', + 'metadata', + 'mpath', + 'path', + 'pattern', + 'polygon', + 'polyline', + 'radialGradient', + 'rect', + 'set', + 'stop', + 'svg', + 'switch', + 'symbol', + 'text', + 'textPath', + 'tspan', + 'use', + 'view', +]; +export const PROP_SUFFIX = '_$p$_'; +export const HOOK_SUFFIX = '_$h$_'; +// The suffix means to bind to context specific key +export const SPECIFIC_CTX_SUFFIX = '_$c$_'; +// The suffix means to bind to whole context +export const WHOLE_CTX_SUFFIX = '_$ctx$_'; +export const builtinHooks = ['useContext']; diff --git a/packages/transpiler/babel-inula-next-core/src/generator/compGenerator.ts b/packages/transpiler/babel-inula-next-core/src/generator/compGenerator.ts index 2b8dafe66cf2a4f7cb632a5335efc41ac0cadafd..ac81748e6ae7ce5b7c5b71cd4d798167f545d22c 100644 --- a/packages/transpiler/babel-inula-next-core/src/generator/compGenerator.ts +++ b/packages/transpiler/babel-inula-next-core/src/generator/compGenerator.ts @@ -1,176 +1,174 @@ -import { ComponentNode, HookNode, IRNode, LifeCycle, SubComponentNode } from '../analyze/types'; -import { getBabelApi, types as t } from '@openinula/babel-api'; -import { generateUpdateState } from './updateStateGenerator'; -import { getStates, wrapUpdate } from './utils'; -import { generateUpdateContext, generateUpdateProp } from './updatePropGenerator'; -import { - alterAttributeMap, - defaultAttributeMap, - DID_MOUNT, - DID_UNMOUNT, - importMap, - PROP_SUFFIX, - WILL_MOUNT, - WILL_UNMOUNT, -} from '../constants'; -import { generateView } from '@openinula/view-generator'; -import { getSubComp } from '../utils'; -import { generateSelfId } from './index'; -import { NodePath } from '@babel/core'; - -export function generateLifecycle(root: IRNode, lifecycleType: LifeCycle) { - root.lifecycle[lifecycleType]!.forEach(node => wrapUpdate(generateSelfId(root.level), node, getStates(root))); - return t.arrowFunctionExpression([], t.blockStatement(root.lifecycle[lifecycleType]!)); -} - -function genWillMountCodeBlock(root: IRNode) { - // ---- Get update views will avoke the willMount and return the updateView function. - let getUpdateViewsFnBody: t.Statement[] = []; - if (root.lifecycle[WILL_MOUNT]) { - root.lifecycle[WILL_MOUNT].forEach(node => wrapUpdate(generateSelfId(root.level), node, getStates(root))); - getUpdateViewsFnBody = root.lifecycle[WILL_MOUNT]; - } - return getUpdateViewsFnBody; -} - -function generateUpdateViewFn(root: ComponentNode<'comp'> | SubComponentNode) { - if (!root.children) { - return null; - } - const subComps = getSubComp(root.variables).map((v): [string, number] => [v.name, v.usedBit]); - const [updateViewFn, nodeInitStmt, templates, topLevelNodes] = generateView(root.children, { - babelApi: getBabelApi(), - importMap, - attributeMap: defaultAttributeMap, - alterAttributeMap, - templateIdx: -1, - subComps, - genTemplateKey: (key: string) => root.fnNode.scope.generateUid(key), - wrapUpdate: (node: t.Statement | t.Expression | null) => { - wrapUpdate(generateSelfId(root.level), node, getStates(root)); - }, - }); - const getUpdateViewsFnBody = genWillMountCodeBlock(root); - if (nodeInitStmt.length) { - getUpdateViewsFnBody.push(...nodeInitStmt); - } - const program = root.fnNode.scope.getProgramParent().path as NodePath; - for (let i = templates.length - 1; i >= 0; i--) { - program.unshiftContainer('body', templates[i]); - } - return getUpdateViewsFnBody.length || topLevelNodes.elements.length - ? t.arrowFunctionExpression( - [], - t.blockStatement([...getUpdateViewsFnBody, t.returnStatement(t.arrayExpression([topLevelNodes, updateViewFn]))]) - ) - : null; -} - -/** - * Generate the update hook function - * ```js - * updateHook: changed => { - * if (changed & 1) { - * self.emitUpdate(); - * } - * } - * ``` - * @param root - */ -function generateUpdateHookFn(root: HookNode) { - const getUpdateViewsFnBody = genWillMountCodeBlock(root); - const children = root.children!; - const paramId = t.identifier('$changed'); - const hookUpdateFn = t.functionExpression( - null, - [paramId], - t.blockStatement([ - t.ifStatement( - t.binaryExpression('&', paramId, t.numericLiteral(Number(children.depMask))), - t.expressionStatement( - t.callExpression(t.memberExpression(generateSelfId(root.level), t.identifier('emitUpdate')), []) - ) - ), - ]) - ); - const fnBody: t.Statement[] = []; - if (getUpdateViewsFnBody.length) { - fnBody.push(...getUpdateViewsFnBody); - } - if (hookUpdateFn) { - fnBody.push(t.returnStatement(hookUpdateFn)); - } - - return fnBody.length ? t.arrowFunctionExpression([], t.blockStatement(fnBody)) : null; -} - -/** - * @View - * self = Inula.createComponent({ - * willMount: () => {}, - * baseNode: ${nodePrefix}0, - * getUpdateViews: () => { - * return [[$node0],changed => { - * if (changed & 1) $node1.update(); - * }], - * }, - * updateProp: (propName, newValue) => {}, - * updateState: (changed) => {} - * }) - */ -export function generateComp(root: ComponentNode | HookNode | SubComponentNode) { - const compInitializerNode = t.objectExpression([]); - const addProperty = (key: string, value: t.Expression | null) => { - if (value === null) return; - compInitializerNode.properties.push(t.objectProperty(t.identifier(key), value)); - }; - - const nodeCtor = root.type === 'hook' ? t.identifier(importMap.createHook) : t.identifier(importMap.createComponent); - const node = t.expressionStatement( - t.assignmentExpression('=', generateSelfId(root.level), t.callExpression(nodeCtor, [compInitializerNode])) - ); - - const result: t.Statement[] = [node]; - - // ---- Lifecycle - // WILL_MOUNT add to function body directly, because it should be executed immediately - const lifecycleNames = [DID_MOUNT, WILL_UNMOUNT, DID_UNMOUNT] as const; - lifecycleNames.forEach((lifecycleType: LifeCycle) => { - if (root.lifecycle[lifecycleType]?.length) { - addProperty(lifecycleType, generateLifecycle(root, lifecycleType)); - } - }); - - // ---- Update state - addProperty('updateState', generateUpdateState(root)); - - // ---- Update props - const updatePropsFn = generateUpdateProp(root, PROP_SUFFIX); - if (updatePropsFn) { - addProperty('updateProp', updatePropsFn); - } - - // ---- Update context - const updateContextFn = generateUpdateContext(root); - if (updateContextFn) { - addProperty('updateContext', updateContextFn); - } - - // ---- Update views - if (root.children) { - let getUpdateViews: t.ArrowFunctionExpression | null; - if (root.type === 'hook') { - getUpdateViews = generateUpdateHookFn(root); - - addProperty('value', t.arrowFunctionExpression([], root.children.value)); - } else { - getUpdateViews = generateUpdateViewFn(root); - } - - if (getUpdateViews) { - addProperty('getUpdateViews', getUpdateViews); - } - } - - return result; -} +import { ComponentNode, HookNode, IRNode, LifeCycle, SubComponentNode } from '../analyze/types'; +import { getBabelApi, types as t } from '@openinula/babel-api'; +import { generateUpdateState } from './updateStateGenerator'; +import { getStates, wrapUpdate } from './utils'; +import { generateUpdateContext, generateUpdateProp } from './updatePropGenerator'; +import { + alterAttributeMap, + defaultAttributeMap, + DID_MOUNT, + DID_UNMOUNT, + importMap, + PROP_SUFFIX, + WILL_MOUNT, + WILL_UNMOUNT, +} from '../constants'; +import { generateView } from '@openinula/view-generator'; +import { getSubComp } from '../utils'; +import { generateSelfId } from './index'; +import { NodePath } from '@babel/core'; + +export function generateLifecycle(root: IRNode, lifecycleType: LifeCycle) { + root.lifecycle[lifecycleType]!.forEach(node => wrapUpdate(generateSelfId(root.level), node, getStates(root))); + return t.arrowFunctionExpression([], t.blockStatement(root.lifecycle[lifecycleType]!)); +} + +function genWillMountCodeBlock(root: IRNode) { + // ---- Get update views will avoke the willMount and return the updateView function. + let getUpdateViewsFnBody: t.Statement[] = []; + if (root.lifecycle[WILL_MOUNT]) { + root.lifecycle[WILL_MOUNT].forEach(node => wrapUpdate(generateSelfId(root.level), node, getStates(root))); + getUpdateViewsFnBody = root.lifecycle[WILL_MOUNT]; + } + return getUpdateViewsFnBody; +} + +function generateUpdateViewFn(root: ComponentNode<'comp'> | SubComponentNode) { + if (!root.children) { + return null; + } + const subComps = getSubComp(root.variables).map((v): [string, number] => [v.name, v.usedBit]); + const [updateViewFn, nodeInitStmt, templates, topLevelNodes] = generateView(root.children, { + babelApi: getBabelApi(), + importMap, + attributeMap: defaultAttributeMap, + alterAttributeMap, + templateIdx: -1, + subComps, + genTemplateKey: (key: string) => root.fnNode.scope.generateUid(key), + wrapUpdate: (node: t.Statement | t.Expression | null) => { + wrapUpdate(generateSelfId(root.level), node, getStates(root)); + }, + }); + const getUpdateViewsFnBody = genWillMountCodeBlock(root); + if (nodeInitStmt.length) { + getUpdateViewsFnBody.push(...nodeInitStmt); + } + const program = root.fnNode.scope.getProgramParent().path as NodePath; + for (let i = templates.length - 1; i >= 0; i--) { + program.unshiftContainer('body', templates[i]); + } + return getUpdateViewsFnBody.length || topLevelNodes.elements.length + ? t.arrowFunctionExpression( + [], + t.blockStatement([...getUpdateViewsFnBody, t.returnStatement(t.arrayExpression([topLevelNodes, updateViewFn]))]) + ) + : null; +} + +/** + * Generate the update hook function + * ```js + * updateHook: changed => { + * if (changed & 1) { + * emitUpdate(self); + * } + * } + * ``` + * @param root + */ +function generateUpdateHookFn(root: HookNode) { + const getUpdateViewsFnBody = genWillMountCodeBlock(root); + const children = root.children!; + const paramId = t.identifier('$changed'); + const hookUpdateFn = t.functionExpression( + null, + [paramId], + t.blockStatement([ + t.ifStatement( + t.binaryExpression('&', paramId, t.numericLiteral(Number(children.depMask))), + t.expressionStatement(t.callExpression(t.identifier(importMap.emitUpdate), [generateSelfId(root.level)])) + ), + ]) + ); + const fnBody: t.Statement[] = []; + if (getUpdateViewsFnBody.length) { + fnBody.push(...getUpdateViewsFnBody); + } + if (hookUpdateFn) { + fnBody.push(t.returnStatement(hookUpdateFn)); + } + + return fnBody.length ? t.arrowFunctionExpression([], t.blockStatement(fnBody)) : null; +} + +/** + * @View + * self = Inula.createComponent({ + * willMount: () => {}, + * baseNode: ${nodePrefix}0, + * getUpdateViews: () => { + * return [[$node0],changed => { + * if (changed & 1) $node1.update(); + * }], + * }, + * updateProp: (propName, newValue) => {}, + * updateState: (changed) => {} + * }) + */ +export function generateComp(root: ComponentNode | HookNode | SubComponentNode) { + const compInitializerNode = t.objectExpression([]); + const addProperty = (key: string, value: t.Expression | null) => { + if (value === null) return; + compInitializerNode.properties.push(t.objectProperty(t.identifier(key), value)); + }; + + const nodeCtor = root.type === 'hook' ? t.identifier(importMap.createHook) : t.identifier(importMap.createComponent); + const node = t.expressionStatement( + t.assignmentExpression('=', generateSelfId(root.level), t.callExpression(nodeCtor, [compInitializerNode])) + ); + + const result: t.Statement[] = [node]; + + // ---- Lifecycle + // WILL_MOUNT add to function body directly, because it should be executed immediately + const lifecycleNames = [DID_MOUNT, WILL_UNMOUNT, DID_UNMOUNT] as const; + lifecycleNames.forEach((lifecycleType: LifeCycle) => { + if (root.lifecycle[lifecycleType]?.length) { + addProperty(lifecycleType, generateLifecycle(root, lifecycleType)); + } + }); + + // ---- Update state + addProperty('updateState', generateUpdateState(root)); + + // ---- Update props + const updatePropsFn = generateUpdateProp(root, PROP_SUFFIX); + if (updatePropsFn) { + addProperty('updateProp', updatePropsFn); + } + + // ---- Update context + const updateContextFn = generateUpdateContext(root); + if (updateContextFn) { + addProperty('updateContext', updateContextFn); + } + + // ---- Update views + if (root.children) { + let getUpdateViews: t.ArrowFunctionExpression | null; + if (root.type === 'hook') { + getUpdateViews = generateUpdateHookFn(root); + + addProperty('value', t.arrowFunctionExpression([], root.children.value)); + } else { + getUpdateViews = generateUpdateViewFn(root); + } + + if (getUpdateViews) { + addProperty('getUpdateViews', getUpdateViews); + } + } + + return result; +} diff --git a/packages/transpiler/babel-inula-next-core/src/generator/index.ts b/packages/transpiler/babel-inula-next-core/src/generator/index.ts index 5344c49681f72f73e76ddd75fa38305606c9ce0f..7f9945996800e89b975a634ebe9c8a9f97218fc9 100644 --- a/packages/transpiler/babel-inula-next-core/src/generator/index.ts +++ b/packages/transpiler/babel-inula-next-core/src/generator/index.ts @@ -1,60 +1,58 @@ -import { ComponentNode, HookNode, SubComponentNode, Variable } from '../analyze/types'; -import { types as t } from '@openinula/babel-api'; -import { generateComp } from './compGenerator'; -import { getStates, wrapUpdate } from './utils'; -import { HOOK_SUFFIX } from '../constants'; - -function reconstructVariable(variable: Variable) { - if (variable.type === 'reactive') { - // ---- If it is a computed, we initiate and modify it in `updateState` - const kind = variable.dependency ? 'let' : variable.kind; - const value = variable.dependency ? null : variable.value; - if (variable.name.endsWith(HOOK_SUFFIX)) { - (variable.value as t.CallExpression).arguments.push(t.numericLiteral(variable.bit!)); - } - return t.variableDeclaration(kind, [t.variableDeclarator(t.identifier(variable.name), value)]); - } - - if (variable.type === 'plain') { - return variable.value; - } - - // --- SubComp - return generate(variable); -} - -export function generate(root: ComponentNode | SubComponentNode | HookNode): t.FunctionDeclaration { - const states = getStates(root); - - // ---- Wrap each variable with update - root.variables.forEach(variable => { - if (variable.type === 'subComp') return; - wrapUpdate(generateSelfId(root.level), variable.value, states); - }); - - const compNode = t.functionDeclaration(t.identifier(root.name), root.params, t.blockStatement([])); - - const addStatement = (...statements: t.Statement[]) => { - compNode.body.body.push(...statements); - }; - - // ---- Declare self - addStatement(t.variableDeclaration('let', [t.variableDeclarator(generateSelfId(root.level))])); - - // ---- Reconstruct the variables - addStatement(...root.variables.map(reconstructVariable)); - - // ---- Add comp - addStatement(...generateComp(root)); - - // ---- Add return self.init() - addStatement( - t.returnStatement(t.callExpression(t.memberExpression(generateSelfId(root.level), t.identifier('init')), [])) - ); - - return compNode; -} - -export function generateSelfId(level: number) { - return t.identifier(`self${level ? level : ''}`); -} +import { ComponentNode, HookNode, SubComponentNode, Variable } from '../analyze/types'; +import { types as t } from '@openinula/babel-api'; +import { generateComp } from './compGenerator'; +import { getStates, wrapUpdate } from './utils'; +import { HOOK_SUFFIX, importMap } from '../constants'; + +function reconstructVariable(variable: Variable) { + if (variable.type === 'reactive') { + // ---- If it is a computed, we initiate and modify it in `updateState` + const kind = variable.dependency ? 'let' : variable.kind; + const value = variable.dependency ? null : variable.value; + if (variable.name.endsWith(HOOK_SUFFIX)) { + (variable.value as t.CallExpression).arguments.push(t.numericLiteral(variable.bit!)); + } + return t.variableDeclaration(kind, [t.variableDeclarator(t.identifier(variable.name), value)]); + } + + if (variable.type === 'plain') { + return variable.value; + } + + // --- SubComp + return generate(variable); +} + +export function generate(root: ComponentNode | SubComponentNode | HookNode): t.FunctionDeclaration { + const states = getStates(root); + + // ---- Wrap each variable with update + root.variables.forEach(variable => { + if (variable.type === 'subComp') return; + wrapUpdate(generateSelfId(root.level), variable.value, states); + }); + + const compNode = t.functionDeclaration(t.identifier(root.name), root.params, t.blockStatement([])); + + const addStatement = (...statements: t.Statement[]) => { + compNode.body.body.push(...statements); + }; + + // ---- Declare self + addStatement(t.variableDeclaration('let', [t.variableDeclarator(generateSelfId(root.level))])); + + // ---- Reconstruct the variables + addStatement(...root.variables.map(reconstructVariable)); + + // ---- Add comp + addStatement(...generateComp(root)); + + // ---- Add return self.init() + addStatement(t.returnStatement(t.callExpression(t.identifier(importMap.initCompNode), [generateSelfId(root.level)]))); + + return compNode; +} + +export function generateSelfId(level: number) { + return t.identifier(`self${level ? level : ''}`); +} diff --git a/packages/transpiler/babel-inula-next-core/src/generator/updatePropGenerator.ts b/packages/transpiler/babel-inula-next-core/src/generator/updatePropGenerator.ts index 5038f2e9fcaba90378dab80eed1f22f3aafa7975..6ae88a35b0925b92e6a654ab1ee6da45f6b4f94a 100644 --- a/packages/transpiler/babel-inula-next-core/src/generator/updatePropGenerator.ts +++ b/packages/transpiler/babel-inula-next-core/src/generator/updatePropGenerator.ts @@ -1,150 +1,150 @@ -import { types as t } from '@openinula/babel-api'; -import { ComponentNode, IRNode, ReactiveVariable } from '../analyze/types'; -import { getStates, wrapUpdate } from './utils'; -import { PROP_SUFFIX, SPECIFIC_CTX_SUFFIX, WHOLE_CTX_SUFFIX } from '../constants'; -import { generateSelfId } from './index'; - -/** - * @View - * updateProp: (propName, newValue) => { - * if (propName === 'prop1') { - * xxx - * } else if (propName === 'prop2') { - * xxx - * } - * } - */ -export function generateUpdateProp(root: IRNode, suffix: string) { - const props = root.variables.filter(v => v.type === 'reactive' && v.name.endsWith(suffix)) as ReactiveVariable[]; - if (!props.length) { - return null; - } - const propNodes: [string, t.ExpressionStatement][] = props.map(prop => { - const propName = prop.name.replace(suffix, ''); - const updateNode = t.expressionStatement( - t.assignmentExpression('=', t.identifier(prop.name), t.identifier('newValue')) - ); - wrapUpdate(generateSelfId(root.level), updateNode, getStates(root)); - return [propName, updateNode]; - }); - - const ifNode = propNodes.reduce((acc, cur) => { - const [propName, updateNode] = cur; - const ifNode = t.ifStatement( - t.binaryExpression('===', t.identifier('propName'), t.stringLiteral(propName)), - t.blockStatement([updateNode]), - acc - ); - return ifNode; - }, null); - - const node = t.arrowFunctionExpression( - [t.identifier('propName'), t.identifier('newValue')], - t.blockStatement(ifNode ? [ifNode] : []) - ); - - return node; -} - -/** - * Aggregate the context variable of the same context into a map - * Key is context name - * Value is the context variable arrays - * @param contextVars - */ -function getContextMap(contextVars: ReactiveVariable[]) { - const contextMap = new Map(); - contextVars.forEach(v => { - const useContextCallExpression = v.value; - if (!t.isCallExpression(useContextCallExpression)) { - throw new Error('Context value error, should be useContext()'); - } - const contextId = useContextCallExpression.arguments[0]; - if (!t.isIdentifier(contextId)) { - throw new Error('The first argument of UseContext should be an identifier'); - } - const contextName = contextId.name; - if (!contextMap.has(contextName)) { - contextMap.set(contextName, []); - } - contextMap.get(contextName)!.push(v); - }); - return contextMap; -} - -/** - * Generate context update methods - * ```js - * function App() { - * let level_$c$_ = useContext(UserContext, 'level') - * let path_$c$_ = useContext(UserContext, 'path') - * let user_$ctx$_ = useContext(UserContext) - * } - * // turn into - * function App() { - * self = { - * updateContext: (ctx, key, value) => { - * if (ctx === UserContext) { - * if (key === 'level') { - * level_$c$_ = value; - * } - * if (key === 'path') { - * path_$c$_ = value; - * } - * user_$ctx$_ = value; - * } - * } - * } - * } - * @param root - * @param suffix - */ -export function generateUpdateContext(root: IRNode) { - const contextVars = root.variables.filter( - v => v.type === 'reactive' && (v.name.endsWith(SPECIFIC_CTX_SUFFIX) || v.name.endsWith(WHOLE_CTX_SUFFIX)) - ) as ReactiveVariable[]; - - if (!contextVars.length) { - return null; - } - - const contextMap = getContextMap(contextVars); - - // The parameters of updateContext - const contextParamId = t.identifier('ctx'); - const keyParamId = t.identifier('key'); - const valParamId = t.identifier('value'); - - // The if statements for context to update. - const contextUpdateStmts: t.IfStatement[] = []; - contextMap.forEach((vars, contextName) => { - const specificKeyedCtxVars = vars.filter(v => v.name.endsWith(SPECIFIC_CTX_SUFFIX)); - const wholeCtxVar = vars.find(v => v.name.endsWith(WHOLE_CTX_SUFFIX)); - - const updateStmts: t.Statement[] = specificKeyedCtxVars.map(v => { - const keyName = v.name.replace(SPECIFIC_CTX_SUFFIX, ''); - const updateAssign = t.expressionStatement(t.assignmentExpression('=', t.identifier(v.name), valParamId)); - wrapUpdate(generateSelfId(root.level), updateAssign, getStates(root)); - - // Gen code like: if (key === 'level') { level_$c$_ = value } - return t.ifStatement( - t.binaryExpression('===', keyParamId, t.stringLiteral(keyName)), - t.blockStatement([updateAssign]) - ); - }); - - if (wholeCtxVar) { - const updateWholeContextAssignment = t.expressionStatement( - t.assignmentExpression('=', t.identifier(wholeCtxVar.name), valParamId) - ); - wrapUpdate(generateSelfId(root.level), updateWholeContextAssignment, getStates(root)); - updateStmts.push(updateWholeContextAssignment); - } - - contextUpdateStmts.push( - t.ifStatement(t.binaryExpression('===', contextParamId, t.identifier(contextName)), t.blockStatement(updateStmts)) - ); - }); - - return t.arrowFunctionExpression([contextParamId, keyParamId, valParamId], t.blockStatement(contextUpdateStmts)); -} +import { types as t } from '@openinula/babel-api'; +import { ComponentNode, IRNode, ReactiveVariable } from '../analyze/types'; +import { getStates, wrapUpdate } from './utils'; +import { PROP_SUFFIX, SPECIFIC_CTX_SUFFIX, WHOLE_CTX_SUFFIX } from '../constants'; +import { generateSelfId } from './index'; + +/** + * @View + * updateProp: (propName, newValue) => { + * if (propName === 'prop1') { + * xxx + * } else if (propName === 'prop2') { + * xxx + * } + * } + */ +export function generateUpdateProp(root: IRNode, suffix: string) { + const props = root.variables.filter(v => v.type === 'reactive' && v.name.endsWith(suffix)) as ReactiveVariable[]; + if (!props.length) { + return null; + } + const propNodes: [string, t.ExpressionStatement][] = props.map(prop => { + const propName = prop.name.replace(suffix, ''); + const updateNode = t.expressionStatement( + t.assignmentExpression('=', t.identifier(prop.name), t.identifier('newValue')) + ); + wrapUpdate(generateSelfId(root.level), updateNode, getStates(root)); + return [propName, updateNode]; + }); + + const ifNode = propNodes.reduce((acc, cur) => { + const [propName, updateNode] = cur; + const ifNode = t.ifStatement( + t.binaryExpression('===', t.identifier('propName'), t.stringLiteral(propName)), + t.blockStatement([updateNode]), + acc + ); + return ifNode; + }, null); + + const node = t.arrowFunctionExpression( + [t.identifier('propName'), t.identifier('newValue')], + t.blockStatement(ifNode ? [ifNode] : []) + ); + + return node; +} + +/** + * Aggregate the context variable of the same context into a map + * Key is context name + * Value is the context variable arrays + * @param contextVars + */ +function getContextMap(contextVars: ReactiveVariable[]) { + const contextMap = new Map(); + contextVars.forEach(v => { + const useContextCallExpression = v.value; + if (!t.isCallExpression(useContextCallExpression)) { + throw new Error('Context value error, should be useContext()'); + } + const contextId = useContextCallExpression.arguments[0]; + if (!t.isIdentifier(contextId)) { + throw new Error('The first argument of UseContext should be an identifier'); + } + const contextName = contextId.name; + if (!contextMap.has(contextName)) { + contextMap.set(contextName, []); + } + contextMap.get(contextName)!.push(v); + }); + return contextMap; +} + +/** + * Generate context update methods + * ```js + * function App() { + * let level_$c$_ = useContext(UserContext, 'level') + * let path_$c$_ = useContext(UserContext, 'path') + * let user_$ctx$_ = useContext(UserContext) + * } + * // turn into + * function App() { + * self = { + * updateContext: (ctx, key, value) => { + * if (ctx === UserContext) { + * if (key === 'level') { + * level_$c$_ = value; + * } + * if (key === 'path') { + * path_$c$_ = value; + * } + * user_$ctx$_ = value; + * } + * } + * } + * } + * @param root + * @param suffix + */ +export function generateUpdateContext(root: IRNode) { + const contextVars = root.variables.filter( + v => v.type === 'reactive' && (v.name.endsWith(SPECIFIC_CTX_SUFFIX) || v.name.endsWith(WHOLE_CTX_SUFFIX)) + ) as ReactiveVariable[]; + + if (!contextVars.length) { + return null; + } + + const contextMap = getContextMap(contextVars); + + // The parameters of updateContext + const contextParamId = t.identifier('ctx'); + const keyParamId = t.identifier('key'); + const valParamId = t.identifier('value'); + + // The if statements for context to update. + const contextUpdateStmts: t.IfStatement[] = []; + contextMap.forEach((vars, contextName) => { + const specificKeyedCtxVars = vars.filter(v => v.name.endsWith(SPECIFIC_CTX_SUFFIX)); + const wholeCtxVar = vars.find(v => v.name.endsWith(WHOLE_CTX_SUFFIX)); + + const updateStmts: t.Statement[] = specificKeyedCtxVars.map(v => { + const keyName = v.name.replace(SPECIFIC_CTX_SUFFIX, ''); + const updateAssign = t.expressionStatement(t.assignmentExpression('=', t.identifier(v.name), valParamId)); + wrapUpdate(generateSelfId(root.level), updateAssign, getStates(root)); + + // Gen code like: if (key === 'level') { level_$c$_ = value } + return t.ifStatement( + t.binaryExpression('===', keyParamId, t.stringLiteral(keyName)), + t.blockStatement([updateAssign]) + ); + }); + + if (wholeCtxVar) { + const updateWholeContextAssignment = t.expressionStatement( + t.assignmentExpression('=', t.identifier(wholeCtxVar.name), valParamId) + ); + wrapUpdate(generateSelfId(root.level), updateWholeContextAssignment, getStates(root)); + updateStmts.push(updateWholeContextAssignment); + } + + contextUpdateStmts.push( + t.ifStatement(t.binaryExpression('===', contextParamId, t.identifier(contextName)), t.blockStatement(updateStmts)) + ); + }); + + return t.arrowFunctionExpression([contextParamId, keyParamId, valParamId], t.blockStatement(contextUpdateStmts)); +} diff --git a/packages/transpiler/babel-inula-next-core/src/generator/updateStateGenerator.ts b/packages/transpiler/babel-inula-next-core/src/generator/updateStateGenerator.ts index 6c98a1b87cdeb599aad33519bf63c4bb02e0cebd..00267ee1a690569fe01c2d40eebfa238c9ae600f 100644 --- a/packages/transpiler/babel-inula-next-core/src/generator/updateStateGenerator.ts +++ b/packages/transpiler/babel-inula-next-core/src/generator/updateStateGenerator.ts @@ -1,107 +1,108 @@ -import { types as t } from '@openinula/babel-api'; -import { Bitmap } from '@openinula/reactivity-parser'; -import { IRNode, Variable, WatchFunc } from '../analyze/types'; -import { getStates, wrapCheckCache, wrapUpdate } from './utils'; -import { HOOK_SUFFIX } from '../constants'; -import { importMap } from '../constants'; -import { generateSelfId } from './index'; - -/** - * wrap stmt in runOnce function, like - * runOnce(() => log(1)); - * @param stmt - */ -function wrapRunOnce(stmt: t.Statement) { - return t.expressionStatement( - t.callExpression(t.identifier(importMap.runOnce), [t.arrowFunctionExpression([], t.blockStatement([stmt]))]) - ); -} - -export function generateUpdateState(root: IRNode) { - const states = getStates(root); - const blockNode = t.blockStatement([]); - - // ---- Merge same bits and same dep keys - const updates: Record = {}; - - const getDepStatements = (bitMap: number, depNode: t.ArrayExpression | null) => { - const cacheMap = updates[bitMap]; - for (const [key, statements] of cacheMap) { - if (key === null && depNode === null) return statements; - if (t.isNodesEquivalent(key!, depNode!)) return statements; - } - const statements: t.Statement[] = []; - cacheMap.push([depNode, statements]); - return statements; - }; - - const variableAddUpdate = (variable: Variable) => { - if (variable.type !== 'reactive') return; - const bitMap = variable.dependency?.depMask; - if (!bitMap) return; - /** - * @View - * variable = ${value} - */ - let updateNode = t.expressionStatement( - t.assignmentExpression('=', t.identifier(variable.name), variable.value ?? t.nullLiteral()) - ); - if (variable.name.endsWith(HOOK_SUFFIX)) { - updateNode = wrapRunOnce(updateNode); - } else { - wrapUpdate(generateSelfId(root.level), updateNode, states); - } - - const depsNode = variable.dependency!.dependenciesNode; - if (!updates[bitMap]) { - updates[bitMap] = []; - } - getDepStatements(bitMap, depsNode).push(updateNode); - }; - - root.variables.forEach(variableAddUpdate); - - // ---- Add watch - const addWatch = (watch: WatchFunc) => { - if (!watch.dependency) return; - const bitMap = watch.dependency.depMask!; - let updateNode: t.Statement | t.Expression = watch.callback.node.body; - if (t.isExpression(updateNode)) updateNode = t.expressionStatement(updateNode); - wrapUpdate(generateSelfId(root.level), updateNode, states); - - const depsNode = watch.dependency.dependenciesNode; - if (!updates[bitMap]) { - updates[bitMap] = []; - } - - getDepStatements(bitMap, depsNode).push(updateNode); - }; - - root.watch?.forEach(addWatch); - - /** - * @view - * if (changed & bit) { - * ${statements} - * } - */ - const addUpdate = (bit: Bitmap, statements: t.Statement[]) => { - blockNode.body.push( - t.ifStatement( - t.binaryExpression('&', t.identifier('changed'), t.numericLiteral(bit)), - t.blockStatement(statements) - ) - ); - }; - - for (const [bit, cacheMap] of Object.entries(updates)) { - for (const [depsNode, statements] of cacheMap) { - addUpdate( - Number(bit), - depsNode ? [wrapCheckCache(generateSelfId(root.level), depsNode, statements)] : statements - ); - } - } - - return t.arrowFunctionExpression([t.identifier('changed')], blockNode); -} +import { types as t } from '@openinula/babel-api'; +import { Bitmap } from '@openinula/reactivity-parser'; +import { IRNode, Variable, WatchFunc } from '../analyze/types'; +import { getStates, wrapCheckCache, wrapUpdate } from './utils'; +import { HOOK_SUFFIX } from '../constants'; +import { importMap } from '../constants'; +import { generateSelfId } from './index'; + +/** + * wrap stmt in runOnce function, like + * runOnce(() => log(1)); + * @param stmt + */ +function wrapRunOnce(stmt: t.Statement) { + return t.expressionStatement( + t.callExpression(t.identifier(importMap.runOnce), [t.arrowFunctionExpression([], t.blockStatement([stmt]))]) + ); +} + +export function generateUpdateState(root: IRNode) { + const states = getStates(root); + const blockNode = t.blockStatement([]); + + // ---- Merge same bits and same dep keys + const updates: Record = {}; + + const getDepStatements = (bitMap: number, depNode: t.ArrayExpression | null) => { + const cacheMap = updates[bitMap]; + for (const [key, statements] of cacheMap) { + if (key === null && depNode === null) return statements; + if (t.isNodesEquivalent(key!, depNode!)) return statements; + } + const statements: t.Statement[] = []; + cacheMap.push([depNode, statements]); + return statements; + }; + + const variableAddUpdate = (variable: Variable) => { + if (variable.type !== 'reactive') return; + const bitMap = variable.dependency?.depMask; + if (!bitMap) return; + /** + * @View + * variable = ${value} + */ + let updateNode = t.expressionStatement( + t.assignmentExpression('=', t.identifier(variable.name), variable.value ?? t.nullLiteral()) + ); + if (variable.name.endsWith(HOOK_SUFFIX)) { + updateNode = wrapRunOnce(updateNode); + } else { + wrapUpdate(generateSelfId(root.level), updateNode, states); + } + + const depsNode = variable.dependency!.dependenciesNode; + if (!updates[bitMap]) { + updates[bitMap] = []; + } + getDepStatements(bitMap, depsNode).push(updateNode); + }; + + root.variables.forEach(variableAddUpdate); + + // ---- Add watch + const addWatch = (watch: WatchFunc) => { + if (!watch.dependency) return; + const bitMap = watch.dependency.depMask!; + let updateNode: t.Statement | t.Expression = watch.callback.node.body; + if (t.isExpression(updateNode)) updateNode = t.expressionStatement(updateNode); + wrapUpdate(generateSelfId(root.level), updateNode, states); + + const depsNode = watch.dependency.dependenciesNode; + if (!updates[bitMap]) { + updates[bitMap] = []; + } + + getDepStatements(bitMap, depsNode).push(updateNode); + }; + + root.watch?.forEach(addWatch); + + /** + * @view + * if (changed & bit) { + * ${statements} + * } + */ + const addUpdate = (bit: Bitmap, statements: t.Statement[]) => { + blockNode.body.push( + t.ifStatement( + t.binaryExpression('&', t.identifier('changed'), t.numericLiteral(bit)), + t.blockStatement(statements) + ) + ); + }; + + let idx = 0; + for (const [bit, cacheMap] of Object.entries(updates)) { + for (const [depsNode, statements] of cacheMap) { + addUpdate( + Number(bit), + depsNode ? [wrapCheckCache(generateSelfId(root.level), depsNode, statements, idx++)] : statements + ); + } + } + + return t.arrowFunctionExpression([t.identifier('changed')], blockNode); +} diff --git a/packages/transpiler/babel-inula-next-core/src/generator/utils.ts b/packages/transpiler/babel-inula-next-core/src/generator/utils.ts index e6dbc1cd11e3d8b34564f2fe5cd57d0d21f36123..ec9931fc76ed0d761d5721e68e022c534aa5e1c4 100644 --- a/packages/transpiler/babel-inula-next-core/src/generator/utils.ts +++ b/packages/transpiler/babel-inula-next-core/src/generator/utils.ts @@ -1,129 +1,132 @@ -import type { NodePath } from '@babel/core'; -import { types as t, traverse } from '@openinula/babel-api'; -import { IRNode, ReactiveVariable } from '../analyze/types'; -import { importMap, reactivityFuncNames } from '../constants'; - -export function uid() { - return t.callExpression(t.memberExpression(t.identifier('Symbol'), t.identifier('for')), [ - t.stringLiteral('inula-cache'), - ]); -} - -/** - * @View - * if (Inula.notCached(self, ${uid}, depNode)) {${blockStatement}} - */ -export function wrapCheckCache(selfId: t.Identifier, cacheNode: t.ArrayExpression, statements: t.Statement[]) { - return t.ifStatement( - t.callExpression(t.identifier(importMap.notCached), [selfId, uid(), cacheNode]), - t.blockStatement(statements) - ); -} - -/** - * @brief Check if it's the left side of an assignment expression, e.g. count = 1 or count++ - * @param path - * @returns assignment expression - */ -export function isAssignmentExpression( - path: NodePath -): NodePath | NodePath | null { - let parentPath = path.parentPath; - while (parentPath && !t.isStatement(parentPath.node)) { - if (parentPath.isAssignmentExpression()) { - if (parentPath.node.left === path.node) return parentPath; - const leftPath = parentPath.get('left') as NodePath; - if (path.isDescendant(leftPath)) return parentPath; - } else if (parentPath.isUpdateExpression()) { - return parentPath; - } - parentPath = parentPath.parentPath; - } - - return null; -} - -/** - * @View - * xxx = yyy => self.updateDerived(xxx = yyy, 1) - */ -export function wrapUpdate(selfId: t.Identifier, node: t.Statement | t.Expression | null, states: ReactiveVariable[]) { - if (!node) return; - const addUpdateDerived = (node: t.CallExpression['arguments'][number], bit: number) => { - // add a call to updateDerived and comment show the bit - const bitNode = t.numericLiteral(bit); - t.addComment(bitNode, 'trailing', `0b${bit.toString(2)}`, false); - return t.callExpression(t.memberExpression(selfId, t.identifier('updateDerived')), [node, bitNode]); - }; - traverse(nodeWrapFile(node), { - Identifier: (path: NodePath) => { - const variable = states.find(v => v.name === path.node.name); - if (!variable) return; - - const assignmentPath = isAssignmentExpression(path); - if (!assignmentPath) return; - - const assignmentNode = assignmentPath.node; - const writingNode = extractWritingPart(assignmentNode); // the variable writing part of the assignment - if (!writingNode) return; - - // ---- Find all the states in the left - const variables: ReactiveVariable[] = []; - traverse(nodeWrapFile(writingNode), { - Identifier: (path: NodePath) => { - const variable = states.find(v => v.name === path.node.name); - if (variable && !variables.find(v => v.name === variable.name)) { - variables.push(variable); - } - }, - }); - const allBits = variables.reduce((acc, cur) => acc | cur.bit!, 0); - assignmentPath.replaceWith(addUpdateDerived(assignmentNode, allBits)); - assignmentPath.skip(); - }, - CallExpression: (path: NodePath) => { - // handle collections mutable methods, like arr.push() - if (!t.isMemberExpression(path.node.callee)) return; - const funcNameNode = path.node.callee.property; - if (!t.isIdentifier(funcNameNode)) return; - if (!reactivityFuncNames.includes(funcNameNode.name)) return; - - // Traverse up the member expression chain to find the root object - let callee = (path.get('callee') as NodePath).get('object'); - while (callee.isMemberExpression()) { - callee = callee.get('object'); - } - - if (callee.isIdentifier()) { - const key = callee.node.name; - - const variable = states.find(v => v.name === key); - if (!variable) return; - path.replaceWith(addUpdateDerived(path.node, variable.bit!)); - } - - path.skip(); - }, - }); -} - -function extractWritingPart(assignmentNode: t.AssignmentExpression | t.UpdateExpression) { - // Handle different types of assignments - if (t.isUpdateExpression(assignmentNode)) { - // For update expressions like ++x or --x - return assignmentNode.argument; - } else if (t.isAssignmentExpression(assignmentNode)) { - // For regular assignments, create a new assignment expression - // with an empty string as right side (placeholder) - return t.assignmentExpression('=', assignmentNode.left, t.stringLiteral('')); - } - return null; -} - -export function getStates(root: IRNode) { - return root.variables.filter(v => v.type === 'reactive' && v.bit) as ReactiveVariable[]; -} - -export function nodeWrapFile(node: t.Expression | t.Statement): t.File { - return t.file(t.program([t.isStatement(node) ? node : t.expressionStatement(node)])); -} +import type { NodePath } from '@babel/core'; +import { types as t, traverse } from '@openinula/babel-api'; +import { IRNode, ReactiveVariable } from '../analyze/types'; +import { importMap, reactivityFuncNames } from '../constants'; + +export function uid(idx: number) { + return t.stringLiteral(`cache${idx}`); +} + +/** + * @View + * if (Inula.notCached(self, ${uid}, depNode)) {${blockStatement}} + */ +export function wrapCheckCache( + selfId: t.Identifier, + cacheNode: t.ArrayExpression, + statements: t.Statement[], + idx: number +) { + return t.ifStatement( + t.callExpression(t.identifier(importMap.notCached), [selfId, uid(idx), cacheNode]), + t.blockStatement(statements) + ); +} + +/** + * @brief Check if it's the left side of an assignment expression, e.g. count = 1 or count++ + * @param path + * @returns assignment expression + */ +export function isAssignmentExpression( + path: NodePath +): NodePath | NodePath | null { + let parentPath = path.parentPath; + while (parentPath && !t.isStatement(parentPath.node)) { + if (parentPath.isAssignmentExpression()) { + if (parentPath.node.left === path.node) return parentPath; + const leftPath = parentPath.get('left') as NodePath; + if (path.isDescendant(leftPath)) return parentPath; + } else if (parentPath.isUpdateExpression()) { + return parentPath; + } + parentPath = parentPath.parentPath; + } + + return null; +} + +/** + * @View + * xxx = yyy => self.updateDerived(xxx = yyy, 1) + */ +export function wrapUpdate(selfId: t.Identifier, node: t.Statement | t.Expression | null, states: ReactiveVariable[]) { + if (!node) return; + const addUpdateDerived = (node: t.CallExpression['arguments'][number], bit: number) => { + // add a call to updateDerived and comment show the bit + const bitNode = t.numericLiteral(bit); + t.addComment(bitNode, 'trailing', `0b${bit.toString(2)}`, false); + return t.callExpression(t.identifier(importMap.updateNode), [selfId, node, bitNode]); + }; + traverse(nodeWrapFile(node), { + Identifier: (path: NodePath) => { + const variable = states.find(v => v.name === path.node.name); + if (!variable) return; + + const assignmentPath = isAssignmentExpression(path); + if (!assignmentPath) return; + + const assignmentNode = assignmentPath.node; + const writingNode = extractWritingPart(assignmentNode); // the variable writing part of the assignment + if (!writingNode) return; + + // ---- Find all the states in the left + const variables: ReactiveVariable[] = []; + traverse(nodeWrapFile(writingNode), { + Identifier: (path: NodePath) => { + const variable = states.find(v => v.name === path.node.name); + if (variable && !variables.find(v => v.name === variable.name)) { + variables.push(variable); + } + }, + }); + const allBits = variables.reduce((acc, cur) => acc | cur.bit!, 0); + assignmentPath.replaceWith(addUpdateDerived(assignmentNode, allBits)); + assignmentPath.skip(); + }, + CallExpression: (path: NodePath) => { + // handle collections mutable methods, like arr.push() + if (!t.isMemberExpression(path.node.callee)) return; + const funcNameNode = path.node.callee.property; + if (!t.isIdentifier(funcNameNode)) return; + if (!reactivityFuncNames.includes(funcNameNode.name)) return; + + // Traverse up the member expression chain to find the root object + let callee = (path.get('callee') as NodePath).get('object'); + while (callee.isMemberExpression()) { + callee = callee.get('object'); + } + + if (callee.isIdentifier()) { + const key = callee.node.name; + + const variable = states.find(v => v.name === key); + if (!variable) return; + path.replaceWith(addUpdateDerived(path.node, variable.bit!)); + } + + path.skip(); + }, + }); +} + +function extractWritingPart(assignmentNode: t.AssignmentExpression | t.UpdateExpression) { + // Handle different types of assignments + if (t.isUpdateExpression(assignmentNode)) { + // For update expressions like ++x or --x + return assignmentNode.argument; + } else if (t.isAssignmentExpression(assignmentNode)) { + // For regular assignments, create a new assignment expression + // with an empty string as right side (placeholder) + return t.assignmentExpression('=', assignmentNode.left, t.stringLiteral('')); + } + return null; +} + +export function getStates(root: IRNode) { + return root.variables.filter(v => v.type === 'reactive' && v.bit) as ReactiveVariable[]; +} + +export function nodeWrapFile(node: t.Expression | t.Statement): t.File { + return t.file(t.program([t.isStatement(node) ? node : t.expressionStatement(node)])); +} diff --git a/packages/transpiler/babel-inula-next-core/src/global.d.ts b/packages/transpiler/babel-inula-next-core/src/global.d.ts index 34bd908cb589e09ce23697ffa8d5468b19d58f01..6f6805bcc6e325eb1fb8f5b665a13890c623ff35 100644 --- a/packages/transpiler/babel-inula-next-core/src/global.d.ts +++ b/packages/transpiler/babel-inula-next-core/src/global.d.ts @@ -1,4 +1,4 @@ -declare module '@babel/plugin-syntax-do-expressions'; -declare module '@babel/plugin-syntax-decorators'; -declare module '@babel/plugin-syntax-jsx'; -declare module '@babel/plugin-syntax-typescript'; +declare module '@babel/plugin-syntax-do-expressions'; +declare module '@babel/plugin-syntax-decorators'; +declare module '@babel/plugin-syntax-jsx'; +declare module '@babel/plugin-syntax-typescript'; diff --git a/packages/transpiler/babel-inula-next-core/src/index.ts b/packages/transpiler/babel-inula-next-core/src/index.ts index ed33e4cadfbdf2eb93a2e8e044c81cb4c9040db8..2c32c01024644f5d5bf2080187b42f572026472c 100644 --- a/packages/transpiler/babel-inula-next-core/src/index.ts +++ b/packages/transpiler/babel-inula-next-core/src/index.ts @@ -1,48 +1,48 @@ -import syntaxDecorators from '@babel/plugin-syntax-decorators'; -import syntaxJSX from '@babel/plugin-syntax-jsx'; -import syntaxTypescript from '@babel/plugin-syntax-typescript'; -import inulaNext from './plugin'; -import { type InulaNextOption } from './types'; -import { type ConfigAPI, type TransformOptions } from '@babel/core'; -import autoNamingPlugin from './sugarPlugins/autoNamingPlugin'; -import forSubComponentPlugin from './sugarPlugins/forSubComponentPlugin'; -import mapping2ForPlugin from './sugarPlugins/mapping2ForPlugin'; -import propsFormatPlugin from './sugarPlugins/propsFormatPlugin'; -import stateDestructuringPlugin from './sugarPlugins/stateDestructuringPlugin'; -import jsxSlicePlugin from './sugarPlugins/jsxSlicePlugin'; -import earlyReturnPlugin from './sugarPlugins/earlyReturnPlugin'; -import contextPlugin from './sugarPlugins/contextPlugin'; -import hookPlugin from './sugarPlugins/hookPlugin'; -import { resetAccessedKeys } from './constants'; - -const resetImport = { - visitor: { - Program: { - enter() { - resetAccessedKeys(); - }, - }, - }, -}; -export default function (_: ConfigAPI, options: InulaNextOption): TransformOptions { - return { - plugins: [ - syntaxJSX.default ?? syntaxJSX, - [syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }], - [syntaxDecorators.default ?? syntaxDecorators, { legacy: true }], - resetImport, - [forSubComponentPlugin, options], - [mapping2ForPlugin, options], - [jsxSlicePlugin, options], - [autoNamingPlugin, options], - [hookPlugin, options], - [propsFormatPlugin, options], - [contextPlugin, options], - [stateDestructuringPlugin, options], - [earlyReturnPlugin, options], - [inulaNext, options], - ], - }; -} - -export { type InulaNextOption }; +import syntaxDecorators from '@babel/plugin-syntax-decorators'; +import syntaxJSX from '@babel/plugin-syntax-jsx'; +import syntaxTypescript from '@babel/plugin-syntax-typescript'; +import inulaNext from './plugin'; +import { type InulaNextOption } from './types'; +import { type ConfigAPI, type TransformOptions } from '@babel/core'; +import autoNamingPlugin from './sugarPlugins/autoNamingPlugin'; +import forSubComponentPlugin from './sugarPlugins/forSubComponentPlugin'; +import mapping2ForPlugin from './sugarPlugins/mapping2ForPlugin'; +import propsFormatPlugin from './sugarPlugins/propsFormatPlugin'; +import stateDestructuringPlugin from './sugarPlugins/stateDestructuringPlugin'; +import jsxSlicePlugin from './sugarPlugins/jsxSlicePlugin'; +import earlyReturnPlugin from './sugarPlugins/earlyReturnPlugin'; +import contextPlugin from './sugarPlugins/contextPlugin'; +import hookPlugin from './sugarPlugins/hookPlugin'; +import { resetAccessedKeys } from './constants'; + +const resetImport = { + visitor: { + Program: { + enter() { + resetAccessedKeys(); + }, + }, + }, +}; +export default function (_: ConfigAPI, options: InulaNextOption): TransformOptions { + return { + plugins: [ + syntaxJSX.default ?? syntaxJSX, + [syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }], + [syntaxDecorators.default ?? syntaxDecorators, { legacy: true }], + resetImport, + [forSubComponentPlugin, options], + [mapping2ForPlugin, options], + [jsxSlicePlugin, options], + [autoNamingPlugin, options], + [hookPlugin, options], + [propsFormatPlugin, options], + [contextPlugin, options], + [stateDestructuringPlugin, options], + [earlyReturnPlugin, options], + [inulaNext, options], + ], + }; +} + +export { type InulaNextOption }; diff --git a/packages/transpiler/babel-inula-next-core/src/plugin.ts b/packages/transpiler/babel-inula-next-core/src/plugin.ts index 4f94710e7f4540b5603226e0c22aa865c07a9963..8da2091f1d147f535240cae44268692dd4aa44c7 100644 --- a/packages/transpiler/babel-inula-next-core/src/plugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/plugin.ts @@ -1,118 +1,123 @@ -import type babel from '@babel/core'; -import type { BabelFile } from '@babel/core'; -import { NodePath, type PluginObj, type types as t } from '@babel/core'; -import { type InulaNextOption } from './types'; -import { defaultAttributeMap, defaultHTMLTags, getAccessedKeys, resetAccessedKeys } from './constants'; -import { analyze } from './analyze'; -import { addImport, extractFnFromMacro, fileAllowed, getMacroType, toArray } from './utils'; -import { register } from '@openinula/babel-api'; -import { generate } from './generator'; -import { ComponentNode, HookNode } from './analyze/types'; - -const ALREADY_COMPILED: WeakSet | Set = new (WeakSet ?? Set)(); - -interface PluginState { - customState: Record; - filename: string; - file: BabelFile; -} - -function transformNode(path: NodePath, htmlTags: string[], state: PluginState) { - if (ALREADY_COMPILED.has(path)) return false; - const type = getMacroType(path); - if (type) { - const componentNode = extractFnFromMacro(path, type); - let name = ''; - // try to get the component name, when parent is a variable declarator - if (path.parentPath.isVariableDeclarator()) { - const lVal = path.parentPath.get('id'); - if (lVal.isIdentifier()) { - name = lVal.node.name; - } else { - console.error(`${type} macro must be assigned to a variable`); - } - } - const root = analyze(type, name, componentNode, { - htmlTags, - }); - - const resultNode = generate(root); - - recordComponentInState(state, name, root); - - replaceWithComponent(path, resultNode); - - return true; - } - - ALREADY_COMPILED.add(path); - return false; -} - -export default function (api: typeof babel, options: InulaNextOption): PluginObj { - const { - files = '**/*.{js,ts,jsx,tsx}', - excludeFiles = '**/{dist,node_modules,lib}/*', - packageName = '@openinula/next', - htmlTags: customHtmlTags = defaultHtmlTags => defaultHtmlTags, - attributeMap = defaultAttributeMap, - } = options; - - const htmlTags = - typeof customHtmlTags === 'function' - ? customHtmlTags(defaultHTMLTags) - : customHtmlTags.includes('*') - ? [...new Set([...defaultHTMLTags, ...customHtmlTags])].filter(tag => tag !== '*') - : customHtmlTags; - - register(api); - return { - name: 'babel-inula-next-core', - visitor: { - Program: { - exit(program, state) { - if (!fileAllowed(state.filename, toArray(files), toArray(excludeFiles))) { - program.skip(); - return; - } - let transformationHappened = false; - - program.traverse({ - CallExpression(path) { - transformationHappened = transformNode(path, htmlTags, state) || transformationHappened; - }, - }); - - if (transformationHappened && !options.skipImport) { - addImport(program.node, getAccessedKeys(), packageName); - } - }, - }, - }, - }; -} - -function replaceWithComponent(path: NodePath, resultNode: t.FunctionDeclaration) { - const variableDeclarationPath = path.parentPath.parentPath!; - const resultNodeId = resultNode.id; - if (resultNodeId) { - // if id exist, use a temp name to avoid error of duplicate declaration - const realFuncName = resultNodeId.name; - resultNodeId.name = path.scope.generateUid('tmp'); - variableDeclarationPath.replaceWith(resultNode); - resultNodeId.name = realFuncName; - } else { - variableDeclarationPath.replaceWith(resultNode); - } -} - -function recordComponentInState(state: PluginState, name: string, componentNode: ComponentNode | HookNode) { - const metadata = state.file.metadata as { components: Record }; - if (metadata.components == null) { - metadata.components = { - [name]: componentNode, - }; - } else { - metadata.components[name] = componentNode; - } -} +import type babel from '@babel/core'; +import type { BabelFile } from '@babel/core'; +import { NodePath, type PluginObj, type types as t } from '@babel/core'; +import { type InulaNextOption } from './types'; +import { defaultAttributeMap, defaultHTMLTags, getAccessedKeys, resetAccessedKeys } from './constants'; +import { analyze } from './analyze'; +import { addImport, extractFnFromMacro, fileAllowed, getMacroType, toArray } from './utils'; +import { register } from '@openinula/babel-api'; +import { generate } from './generator'; +import { ComponentNode, HookNode } from './analyze/types'; + +const ALREADY_COMPILED: WeakSet | Set = new (WeakSet ?? Set)(); + +interface PluginState { + customState: Record; + filename: string; + file: BabelFile; +} + +function transformNode(path: NodePath, htmlTags: string[], state: PluginState) { + if (ALREADY_COMPILED.has(path)) return false; + const type = getMacroType(path); + if (type) { + const componentNode = extractFnFromMacro(path, type); + let name = ''; + // try to get the component name, when parent is a variable declarator + if (path.parentPath.isVariableDeclarator()) { + const lVal = path.parentPath.get('id'); + if (lVal.isIdentifier()) { + name = lVal.node.name; + } else { + console.error(`${type} macro must be assigned to a variable`); + } + } + const root = analyze(type, name, componentNode, { + htmlTags, + }); + + const resultNode = generate(root); + + recordComponentInState(state, name, root); + + replaceWithComponent(path, resultNode); + + return true; + } + + ALREADY_COMPILED.add(path); + return false; +} + +export default function (api: typeof babel, options: InulaNextOption): PluginObj { + const { + files = '**/*.{js,ts,jsx,tsx}', + excludeFiles = '**/{dist,node_modules,lib}/*', + packageName = '@openinula/next', + htmlTags: customHtmlTags = defaultHtmlTags => defaultHtmlTags, + attributeMap = defaultAttributeMap, + } = options; + + const htmlTags = + typeof customHtmlTags === 'function' + ? customHtmlTags(defaultHTMLTags) + : customHtmlTags.includes('*') + ? [...new Set([...defaultHTMLTags, ...customHtmlTags])].filter(tag => tag !== '*') + : customHtmlTags; + + register(api); + return { + name: 'babel-inula-next-core', + visitor: { + Program: { + exit(program, state) { + if (!fileAllowed(state.filename, toArray(files), toArray(excludeFiles))) { + program.skip(); + return; + } + let transformationHappenedInFile = false; + + program.traverse({ + CallExpression(path) { + const transformed = transformNode(path, htmlTags, state); + if (transformed) { + path.skip(); + } + + transformationHappenedInFile = transformed || transformationHappenedInFile; + }, + }); + + if (transformationHappenedInFile && !options.skipImport) { + addImport(program.node, getAccessedKeys(), packageName); + } + }, + }, + }, + }; +} + +function replaceWithComponent(path: NodePath, resultNode: t.FunctionDeclaration) { + const variableDeclarationPath = path.parentPath.parentPath!; + const resultNodeId = resultNode.id; + if (resultNodeId) { + // if id exist, use a temp name to avoid error of duplicate declaration + const realFuncName = resultNodeId.name; + resultNodeId.name = path.scope.generateUid('tmp'); + variableDeclarationPath.replaceWith(resultNode); + resultNodeId.name = realFuncName; + } else { + variableDeclarationPath.replaceWith(resultNode); + } +} + +function recordComponentInState(state: PluginState, name: string, componentNode: ComponentNode | HookNode) { + const metadata = state.file.metadata as { components: Record }; + if (metadata.components == null) { + metadata.components = { + [name]: componentNode, + }; + } else { + metadata.components[name] = componentNode; + } +} 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 d8cd33f3bea81d5fc69b6698923a58b34771938e..eaa475c42f0a81003a211c40955195dcf2c20fa2 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/autoNamingPlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/autoNamingPlugin.ts @@ -1,82 +1,82 @@ -import babel, { NodePath, PluginObj } from '@babel/core'; -import { register, types as t } from '@openinula/babel-api'; -import { createMacroNode, getFnBodyNode, isFnExp } from '../utils'; -import { builtinHooks, COMPONENT, HOOK } from '../constants'; - -const ALREADY_COMPILED: WeakSet | Set = new (WeakSet ?? Set)(); - -/** - * 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) { - if (ALREADY_COMPILED.has(path)) return; - - const { id } = path.node; - const macroNode = getMacroNode(id, path.node.body, path.node.params); - if (macroNode) { - path.replaceWith(macroNode); - } - - ALREADY_COMPILED.add(path); - }, - VariableDeclaration(path: NodePath) { - if (ALREADY_COMPILED.has(path)) return; - - 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), init.params); - - if (macroNode) { - path.replaceWith(macroNode); - } - } - } - - ALREADY_COMPILED.add(path); - }, - }, - }; -} - -function getMacroNode( - id: babel.types.Identifier | null | undefined, - body: t.BlockStatement, - params: t.FunctionExpression['params'] = [] -) { - const macroName = getMacroName(id?.name); - if (macroName) { - return t.variableDeclaration('const', [t.variableDeclarator(id!, createMacroNode(body, macroName, params))]); - } -} - -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) && !builtinHooks.includes(str); -} +import babel, { NodePath, PluginObj } from '@babel/core'; +import { register, types as t } from '@openinula/babel-api'; +import { createMacroNode, getFnBodyNode, isFnExp } from '../utils'; +import { builtinHooks, COMPONENT, HOOK } from '../constants'; + +const ALREADY_COMPILED: WeakSet | Set = new (WeakSet ?? Set)(); + +/** + * 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) { + if (ALREADY_COMPILED.has(path)) return; + + const { id } = path.node; + const macroNode = getMacroNode(id, path.node.body, path.node.params); + if (macroNode) { + path.replaceWith(macroNode); + } + + ALREADY_COMPILED.add(path); + }, + VariableDeclaration(path: NodePath) { + if (ALREADY_COMPILED.has(path)) return; + + 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), init.params); + + if (macroNode) { + path.replaceWith(macroNode); + } + } + } + + ALREADY_COMPILED.add(path); + }, + }, + }; +} + +function getMacroNode( + id: babel.types.Identifier | null | undefined, + body: t.BlockStatement, + params: t.FunctionExpression['params'] = [] +) { + const macroName = getMacroName(id?.name); + if (macroName) { + return t.variableDeclaration('const', [t.variableDeclarator(id!, createMacroNode(body, macroName, params))]); + } +} + +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) && !builtinHooks.includes(str); +} diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/contextPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/contextPlugin.ts index f8aaacef3fa996ae89c7127317867ca85cc4e086..376c4944b2c9658c6581e058701295a132b79a5f 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/contextPlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/contextPlugin.ts @@ -1,162 +1,162 @@ -/* - * 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 { register, types as t } from '@openinula/babel-api'; -import { isHookPath, extractFnFromMacro, ArrowFunctionWithBlock, isCompPath } from '../utils'; -import { COMPONENT, SPECIFIC_CTX_SUFFIX, WHOLE_CTX_SUFFIX } from '../constants'; - -const ALREADY_COMPILED: WeakSet | Set = new (WeakSet ?? Set)(); - -function checkUseContextArgs(init: t.CallExpression) { - if (init.arguments.length !== 1) { - throw new Error('useContext argument must have only one identifier'); - } - const contextArg = init.arguments[0]; - if (!t.isIdentifier(contextArg)) { - throw new Error('useContext argument must be identifier'); - } -} - -function isValidUseContextCallExpression(init: t.Expression | null | undefined): init is t.CallExpression { - return !!init && t.isCallExpression(init) && t.isIdentifier(init.callee) && init.callee.name === 'useContext'; -} - -function tryAppendContextVariable( - declarator: t.VariableDeclarator, - statementPath: NodePath, - init: t.CallExpression -) { - const id = declarator.id; - if (t.isObjectPattern(id)) { - // Object destructuring case - id.properties - .map(prop => { - if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { - const name = prop.key.name; - statementPath.scope.rename(name, `${name}${SPECIFIC_CTX_SUFFIX}`); - const clonedInit = t.cloneNode(init); - clonedInit.arguments.push(t.stringLiteral(name)); - return t.variableDeclarator(t.identifier(`${name}${SPECIFIC_CTX_SUFFIX}`), clonedInit); - } - return null; - }) - .forEach(declarator => { - if (declarator) { - statementPath.insertAfter(t.variableDeclaration('let', [declarator])); - } - }); - } else if (t.isIdentifier(id)) { - // Direct assignment case - const taggedName = `${id.name}${WHOLE_CTX_SUFFIX}`; - statementPath.scope.rename(id.name, taggedName); - statementPath.insertAfter(t.variableDeclaration('let', [t.variableDeclarator(t.identifier(taggedName), init)])); - } -} - -/** - * Transform useContext into tagged value - * - * Object destructuring case, tagged with _$c$_, means to subscribe the specific key - * ```js - * function App() { - * const { level, path } = useContext(UserContext); - * } - * // turn into - * function App() { - * let level_$c$_ = useContext(UserContext, 'level') - * let path_$c$_ = useContext(UserContext, 'path') - * } - * ``` - * - * Other case, tagged with _$ctx$_, means to subscribe the whole context - * ```js - * function App() { - * const user = useContext(UserContext); - * } - * // turn into - * function App() { - * let user_$ctx$_ = useContext(UserContext) - * } - * @param api - * @param options - */ -export default function (api: typeof babel): PluginObj { - register(api); - - return { - visitor: { - CallExpression(path: NodePath) { - if (isHookPath(path) || isCompPath(path)) { - const fnPath = extractFnFromMacro(path, COMPONENT) as - | NodePath - | NodePath; - - if (fnPath && !ALREADY_COMPILED.has(fnPath)) { - ALREADY_COMPILED.add(fnPath); - - const bodyPath = fnPath.get('body'); - if (Array.isArray(bodyPath) || !bodyPath.isBlockStatement()) { - return; - } - const topLevelPaths = bodyPath.get('body'); - if (!topLevelPaths) { - return; - } - topLevelPaths.forEach(statementPath => { - if (statementPath.isVariableDeclaration()) { - const declarations = statementPath.node.declarations.filter(declarator => { - const init = declarator.init; - if (!isValidUseContextCallExpression(init)) { - return true; - } - checkUseContextArgs(init); - tryAppendContextVariable(declarator, statementPath, init); - return false; - }); - - if (declarations.length === 0) { - statementPath.remove(); - } else if (declarations.length !== statementPath.node.declarations.length) { - statementPath.node.declarations = declarations; - } - } - }); - - // Check for non-top-level useContext calls - bodyPath.traverse({ - CallExpression(callPath) { - if (t.isIdentifier(callPath.node.callee) && callPath.node.callee.name === 'useContext') { - if (!callPath.parentPath.isVariableDeclarator()) { - console.error( - `Error: useContext call at ${callPath.node.loc?.start.line}:${callPath.node.loc?.start.column} is not at the variables declaration..` - ); - throw new Error('useContext must be called at the top level of the component.'); - } - if (callPath.parentPath?.parentPath?.parentPath !== bodyPath) { - console.error( - `Error: useContext call at ${callPath.node.loc?.start.line}:${callPath.node.loc?.start.column} is not at the top level of the component. This violates React Hook rules.` - ); - throw new Error('useContext must be called at the top level of the component.'); - } - } - }, - }); - } - } - }, - }, - }; -} +/* + * 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 { register, types as t } from '@openinula/babel-api'; +import { isHookPath, extractFnFromMacro, ArrowFunctionWithBlock, isCompPath } from '../utils'; +import { COMPONENT, SPECIFIC_CTX_SUFFIX, WHOLE_CTX_SUFFIX } from '../constants'; + +const ALREADY_COMPILED: WeakSet | Set = new (WeakSet ?? Set)(); + +function checkUseContextArgs(init: t.CallExpression) { + if (init.arguments.length !== 1) { + throw new Error('useContext argument must have only one identifier'); + } + const contextArg = init.arguments[0]; + if (!t.isIdentifier(contextArg)) { + throw new Error('useContext argument must be identifier'); + } +} + +function isValidUseContextCallExpression(init: t.Expression | null | undefined): init is t.CallExpression { + return !!init && t.isCallExpression(init) && t.isIdentifier(init.callee) && init.callee.name === 'useContext'; +} + +function tryAppendContextVariable( + declarator: t.VariableDeclarator, + statementPath: NodePath, + init: t.CallExpression +) { + const id = declarator.id; + if (t.isObjectPattern(id)) { + // Object destructuring case + id.properties + .map(prop => { + if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { + const name = prop.key.name; + statementPath.scope.rename(name, `${name}${SPECIFIC_CTX_SUFFIX}`); + const clonedInit = t.cloneNode(init); + clonedInit.arguments.push(t.stringLiteral(name)); + return t.variableDeclarator(t.identifier(`${name}${SPECIFIC_CTX_SUFFIX}`), clonedInit); + } + return null; + }) + .forEach(declarator => { + if (declarator) { + statementPath.insertAfter(t.variableDeclaration('let', [declarator])); + } + }); + } else if (t.isIdentifier(id)) { + // Direct assignment case + const taggedName = `${id.name}${WHOLE_CTX_SUFFIX}`; + statementPath.scope.rename(id.name, taggedName); + statementPath.insertAfter(t.variableDeclaration('let', [t.variableDeclarator(t.identifier(taggedName), init)])); + } +} + +/** + * Transform useContext into tagged value + * + * Object destructuring case, tagged with _$c$_, means to subscribe the specific key + * ```js + * function App() { + * const { level, path } = useContext(UserContext); + * } + * // turn into + * function App() { + * let level_$c$_ = useContext(UserContext, 'level') + * let path_$c$_ = useContext(UserContext, 'path') + * } + * ``` + * + * Other case, tagged with _$ctx$_, means to subscribe the whole context + * ```js + * function App() { + * const user = useContext(UserContext); + * } + * // turn into + * function App() { + * let user_$ctx$_ = useContext(UserContext) + * } + * @param api + * @param options + */ +export default function (api: typeof babel): PluginObj { + register(api); + + return { + visitor: { + CallExpression(path: NodePath) { + if (isHookPath(path) || isCompPath(path)) { + const fnPath = extractFnFromMacro(path, COMPONENT) as + | NodePath + | NodePath; + + if (fnPath && !ALREADY_COMPILED.has(fnPath)) { + ALREADY_COMPILED.add(fnPath); + + const bodyPath = fnPath.get('body'); + if (Array.isArray(bodyPath) || !bodyPath.isBlockStatement()) { + return; + } + const topLevelPaths = bodyPath.get('body'); + if (!topLevelPaths) { + return; + } + topLevelPaths.forEach(statementPath => { + if (statementPath.isVariableDeclaration()) { + const declarations = statementPath.node.declarations.filter(declarator => { + const init = declarator.init; + if (!isValidUseContextCallExpression(init)) { + return true; + } + checkUseContextArgs(init); + tryAppendContextVariable(declarator, statementPath, init); + return false; + }); + + if (declarations.length === 0) { + statementPath.remove(); + } else if (declarations.length !== statementPath.node.declarations.length) { + statementPath.node.declarations = declarations; + } + } + }); + + // Check for non-top-level useContext calls + bodyPath.traverse({ + CallExpression(callPath) { + if (t.isIdentifier(callPath.node.callee) && callPath.node.callee.name === 'useContext') { + if (!callPath.parentPath.isVariableDeclarator()) { + console.error( + `Error: useContext call at ${callPath.node.loc?.start.line}:${callPath.node.loc?.start.column} is not at the variables declaration..` + ); + throw new Error('useContext must be called at the top level of the component.'); + } + if (callPath.parentPath?.parentPath?.parentPath !== bodyPath) { + console.error( + `Error: useContext call at ${callPath.node.loc?.start.line}:${callPath.node.loc?.start.column} is not at the top level of the component. This violates React Hook rules.` + ); + throw new Error('useContext must be called at the top level of the component.'); + } + } + }, + }); + } + } + }, + }, + }; +} 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 6d186a2d2b991e831f22200e8e9bb7ebf8f539cd..1d3e10c8118865cb0a5443c4be3ad9b7fc27071c 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/earlyReturnPlugin.ts @@ -1,174 +1,174 @@ -/* - * 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 { 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); - - return { - 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 branches = parseBranches( - bodyPath.get('body')[ifStmtIndex] as NodePath, - bodyPath.node.body.slice(ifStmtIndex + 1) - ); - - // 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--; - } - - // 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); - - // 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.node.body : [next.node]; -} - -function hasEarlyReturn(path: NodePath) { - let hasReturn = false; - path.traverse({ - ReturnStatement(path: NodePath) { - if ( - path.parentPath.isFunctionDeclaration() || - path.parentPath.isFunctionExpression() || - path.parentPath.isArrowFunctionExpression() - ) { - return; - } - hasReturn = true; - }, - }); - return hasReturn; -} - -export 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; -} +/* + * 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 { 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); + + return { + 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 branches = parseBranches( + bodyPath.get('body')[ifStmtIndex] as NodePath, + bodyPath.node.body.slice(ifStmtIndex + 1) + ); + + // 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--; + } + + // 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); + + // 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.node.body : [next.node]; +} + +function hasEarlyReturn(path: NodePath) { + let hasReturn = false; + path.traverse({ + ReturnStatement(path: NodePath) { + if ( + path.parentPath.isFunctionDeclaration() || + path.parentPath.isFunctionExpression() || + path.parentPath.isArrowFunctionExpression() + ) { + return; + } + hasReturn = true; + }, + }); + return hasReturn; +} + +export 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/forSubComponentPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/forSubComponentPlugin.ts index 570c5e5d1f75d96efda13cf9b29110bc6e3b7867..f66297c9d16bd1d559946a970b694d7606e26cc2 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/forSubComponentPlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/forSubComponentPlugin.ts @@ -1,111 +1,111 @@ -/* - * 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 { register, types as t } from '@openinula/babel-api'; -import { searchNestedProps } from './stateDestructuringPlugin'; -import { genUid } from './earlyReturnPlugin'; - -/** - * For Sub Component Plugin - * Find For element and convert - * - * @param api - */ - -export default function (api: typeof babel): PluginObj { - register(api); - return { - visitor: { - JSXElement: function (path: NodePath) { - const tagName = path.node.openingElement.name; - - if (t.isJSXIdentifier(tagName) && tagName.name === 'for') { - let jsx: t.Expression; - const children = path.get('children'); - const expContainer: NodePath | undefined = children.find(child => child.isJSXExpressionContainer()); - if (!expContainer) { - return; - } - // 判断箭头函数 - const arrow = expContainer.get('expression'); - if (Array.isArray(arrow)) { - return; - } - if (!arrow.isArrowFunctionExpression()) { - return; - } - const inputs = arrow.get('params'); - - let arrowParams: NodePath[] = []; - if (Array.isArray(inputs)) { - arrowParams = inputs; - } else { - arrowParams.push(inputs); - } - - let params: string[] = []; - arrowParams.forEach(input => { - if (input.isIdentifier()) { - params.push(input.node.name); - } else if (input.isObjectPattern()) { - params = params.concat(searchNestedProps(input)); - } - }); - const body = arrow.node.body; - if (t.isExpression(body)) { - return; - } - if (Array.isArray(body)) { - return; - } - if (body.body.length == 1 && t.isReturnStatement(body.body[0])) { - return; - } - const id = genUid(path.scope, 'For'); - arrow.node.body = t.jsxElement( - t.jsxOpeningElement( - t.jsxIdentifier(id), - params.map(param => - t.jsxAttribute(t.jsxIdentifier(param), t.jsxExpressionContainer(t.identifier(param))) - ), - true - ), - null, - [] - ); - - const func = t.functionDeclaration( - t.identifier(id), - [ - t.objectPattern( - params.map(param => t.objectProperty(t.identifier(param), t.identifier(param), false, true)) - ), - ], - body - ); - let parent: NodePath = path.parentPath; - while (!parent.isReturnStatement()) { - if (!parent.parentPath) { - return; - } - parent = parent.parentPath; - } - parent.insertBefore(func); - } - }, - }, - }; -} +/* + * 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 { register, types as t } from '@openinula/babel-api'; +import { searchNestedProps } from './stateDestructuringPlugin'; +import { genUid } from './earlyReturnPlugin'; + +/** + * For Sub Component Plugin + * Find For element and convert + * + * @param api + */ + +export default function (api: typeof babel): PluginObj { + register(api); + return { + visitor: { + JSXElement: function (path: NodePath) { + const tagName = path.node.openingElement.name; + + if (t.isJSXIdentifier(tagName) && tagName.name === 'for') { + let jsx: t.Expression; + const children = path.get('children'); + const expContainer: NodePath | undefined = children.find(child => child.isJSXExpressionContainer()); + if (!expContainer) { + return; + } + // 判断箭头函数 + const arrow = expContainer.get('expression'); + if (Array.isArray(arrow)) { + return; + } + if (!arrow.isArrowFunctionExpression()) { + return; + } + const inputs = arrow.get('params'); + + let arrowParams: NodePath[] = []; + if (Array.isArray(inputs)) { + arrowParams = inputs; + } else { + arrowParams.push(inputs); + } + + let params: string[] = []; + arrowParams.forEach(input => { + if (input.isIdentifier()) { + params.push(input.node.name); + } else if (input.isObjectPattern()) { + params = params.concat(searchNestedProps(input)); + } + }); + const body = arrow.node.body; + if (t.isExpression(body)) { + return; + } + if (Array.isArray(body)) { + return; + } + if (body.body.length == 1 && t.isReturnStatement(body.body[0])) { + return; + } + const id = genUid(path.scope, 'For'); + arrow.node.body = t.jsxElement( + t.jsxOpeningElement( + t.jsxIdentifier(id), + params.map(param => + t.jsxAttribute(t.jsxIdentifier(param), t.jsxExpressionContainer(t.identifier(param))) + ), + true + ), + null, + [] + ); + + const func = t.functionDeclaration( + t.identifier(id), + [ + t.objectPattern( + params.map(param => t.objectProperty(t.identifier(param), t.identifier(param), false, true)) + ), + ], + body + ); + let parent: NodePath = path.parentPath; + while (!parent.isReturnStatement()) { + if (!parent.parentPath) { + return; + } + parent = parent.parentPath; + } + parent.insertBefore(func); + } + }, + }, + }; +} diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/hookPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/hookPlugin.ts index 0ec75f9ba2d08ba77ffa89c5416b87483f919e1d..1c0c94eab2ff7d64e07da56e12a8e41240116610 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/hookPlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/hookPlugin.ts @@ -1,171 +1,175 @@ -/* - * 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 { register, types as t } from '@openinula/babel-api'; -import { isHookPath, extractFnFromMacro, ArrowFunctionWithBlock, isCompPath, wrapUntrack } from '../utils'; -import { builtinHooks, COMPONENT, HOOK_SUFFIX, importMap } from '../constants'; -import type { Scope } from '@babel/traverse'; - -const ALREADY_COMPILED: WeakSet | Set = new (WeakSet ?? Set)(); - -function isValidArgument(arg: t.Node): arg is t.Expression | t.SpreadElement { - if (t.isExpression(arg) || t.isSpreadElement(arg)) { - return true; - } - throw new Error('should pass expression or spread element as parameter for custom hook'); -} - -/** - * function useMousePosition(baseX, baseY, { settings, id }) { - * -> - * function useMousePosition({p1: baseX, p2: baseY, p3: { settings, id }} - * @param fnPath - */ -function transformHookParams(fnPath: NodePath | NodePath) { - const params = fnPath.node.params; - const objPropertyParam = params.map((param, idx) => - !t.isRestElement(param) ? t.objectProperty(t.identifier(`p${idx}`), param) : param - ); - - fnPath.node.params = [t.objectPattern(objPropertyParam)]; -} - -/** - * Transform use custom hook into createHook - ** ```js - * function App() { - * const [x, y] = useMousePosition(baseX, baseY, { - * settings: 1, - * id: 0 - * }) - * } - * // turn into - * function App() { - * let useMousePosition_$h$_ =[useMousePosition, [baseX, baseY, {settings}]]; - * watch(() => { - * untrack(() => useMousePosition_$h$_).updateProp('p0', baseX) - * }) - * watch(() => { - * untrack(() => useMousePosition_$h$_).updateProp('p1', baseY) - * }) - * watch(() => { - * untrack(() => useMousePosition_$h$_).updateProp('p2', {settings}) - * }) - * let [x, y] = useMousePosition_$h$_.value(); - * } - * ``` - * @param api - * @param options - */ -export default function (api: typeof babel): PluginObj { - register(api); - - return { - visitor: { - CallExpression(path: NodePath) { - if (isHookPath(path) || isCompPath(path)) { - const fnPath = extractFnFromMacro(path, COMPONENT) as - | NodePath - | NodePath; - - if (fnPath && !ALREADY_COMPILED.has(fnPath)) { - ALREADY_COMPILED.add(fnPath); - - // handle the useXXX() - transformUseHookCalling(fnPath, path.scope); - - if (isHookPath(path)) { - transformHookParams(fnPath); - } - } - } - }, - }, - }; -} - -function transformUseHookCalling( - fnPath: NodePath | NodePath, - scope: Scope -) { - const bodyPath = fnPath.get('body'); - if (Array.isArray(bodyPath) || !bodyPath.isBlockStatement()) { - return; - } - const topLevelPaths = bodyPath.get('body'); - if (!topLevelPaths) { - return; - } - topLevelPaths.forEach(statementPath => { - if (statementPath.isVariableDeclaration()) { - const declarator = statementPath.get('declarations')[0]; - const init = declarator.get('init'); - if (init.isCallExpression() && init.get('callee').isIdentifier()) { - const callee = init.node.callee as t.Identifier; - if (callee.name.startsWith('use') && !builtinHooks.includes(callee.name)) { - // Generate a unique identifier for the hook - const hookId = t.identifier(`${scope.generateUid(callee.name)}${HOOK_SUFFIX}`); - - // 过滤并类型检查参数 - const validArguments = init.node.arguments.filter(isValidArgument); - const updateWatchers = generateUpdateWatchers(hookId, validArguments); - // Create the createHook call - const createHookCall = t.callExpression(t.identifier(importMap.useHook), [ - callee, - // wrapUntrack(t.arrayExpression(validArguments)), - t.arrayExpression(validArguments), - ]); - - // Create a new variable declaration for the hook - const hookDeclaration = t.variableDeclaration('let', [t.variableDeclarator(hookId, createHookCall)]); - - // Insert the new declaration and hook updaters before the current statement - statementPath.insertBefore([hookDeclaration, ...updateWatchers]); - - // Replace the original call with hook.return(); - init.replaceWith(t.callExpression(t.memberExpression(hookId, t.identifier('value')), [])); - } - } - } - }); -} - -// Generate update watchers for each argument, which is not static -function generateUpdateWatchers( - hookId: t.Identifier, - validArguments: (babel.types.SpreadElement | babel.types.Expression)[] -) { - return validArguments - .filter(arg => !t.isLiteral(arg)) - .map((arg, idx) => { - const argName = `p${idx}`; - return t.expressionStatement( - t.callExpression(t.identifier('watch'), [ - t.arrowFunctionExpression( - [], - t.blockStatement([ - t.expressionStatement( - t.callExpression(t.memberExpression(wrapUntrack(hookId), t.identifier('updateProp')), [ - t.stringLiteral(argName), - arg, - ]) - ), - ]) - ), - ]) - ); - }); -} +/* + * 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 { register, types as t } from '@openinula/babel-api'; +import { isHookPath, extractFnFromMacro, ArrowFunctionWithBlock, isCompPath, wrapUntrack } from '../utils'; +import { builtinHooks, COMPONENT, HOOK_SUFFIX, importMap } from '../constants'; +import type { Scope } from '@babel/traverse'; + +const ALREADY_COMPILED: WeakSet | Set = new (WeakSet ?? Set)(); +const HOOK_USING_PREFIX = 'use'; + +function isValidArgument(arg: t.Node): arg is t.Expression | t.SpreadElement { + if (t.isExpression(arg) || t.isSpreadElement(arg)) { + return true; + } + throw new Error('should pass expression or spread element as parameter for custom hook'); +} + +/** + * function useMousePosition(baseX, baseY, { settings, id }) { + * -> + * function useMousePosition({p1: baseX, p2: baseY, p3: { settings, id }} + * @param fnPath + */ +function transformHookParams(fnPath: NodePath | NodePath) { + const params = fnPath.node.params; + const objPropertyParam = params.map((param, idx) => + !t.isRestElement(param) ? t.objectProperty(t.identifier(`p${idx}`), param) : param + ); + + fnPath.node.params = [t.objectPattern(objPropertyParam)]; +} + +/** + * Transform use custom hook into createHook + ** ```js + * function App() { + * const [x, y] = useMousePosition(baseX, baseY, { + * settings: 1, + * id: 0 + * }) + * } + * // turn into + * function App() { + * let useMousePosition_$h$_ =[useMousePosition, [baseX, baseY, {settings}]]; + * watch(() => { + * untrack(() => useMousePosition_$h$_).updateProp('p0', baseX) + * }) + * watch(() => { + * untrack(() => useMousePosition_$h$_).updateProp('p1', baseY) + * }) + * watch(() => { + * untrack(() => useMousePosition_$h$_).updateProp('p2', {settings}) + * }) + * let [x, y] = useMousePosition_$h$_.value(); + * } + * ``` + * @param api + * @param options + */ +export default function (api: typeof babel): PluginObj { + register(api); + + return { + visitor: { + CallExpression(path: NodePath) { + if (isHookPath(path) || isCompPath(path)) { + const fnPath = extractFnFromMacro(path, COMPONENT) as + | NodePath + | NodePath; + + if (fnPath && !ALREADY_COMPILED.has(fnPath)) { + ALREADY_COMPILED.add(fnPath); + + // handle the useXXX() + transformUseHookCalling(fnPath, path.scope); + + if (isHookPath(path)) { + transformHookParams(fnPath); + } + } + } + }, + }, + }; +} + +function transformUseHookCalling( + fnPath: NodePath | NodePath, + scope: Scope +) { + const bodyPath = fnPath.get('body'); + if (Array.isArray(bodyPath) || !bodyPath.isBlockStatement()) { + return; + } + const topLevelPaths = bodyPath.get('body'); + if (!topLevelPaths) { + return; + } + topLevelPaths.forEach(statementPath => { + if (statementPath.isVariableDeclaration()) { + const declarator = statementPath.get('declarations')[0]; + const init = declarator.get('init'); + if (init.isCallExpression() && init.get('callee').isIdentifier()) { + const callee = init.node.callee as t.Identifier; + if (callee.name.startsWith(HOOK_USING_PREFIX) && !builtinHooks.includes(callee.name)) { + // Generate a unique identifier for the hook + const hookId = t.identifier(`${scope.generateUid(callee.name)}${HOOK_SUFFIX}`); + + // 过滤并类型检查参数 + const validArguments = init.node.arguments.filter(isValidArgument); + const updateWatchers = generateUpdateWatchers(hookId, validArguments); + // Create the createHook call + const createHookCall = t.callExpression(t.identifier(importMap.useHook), [ + callee, + t.arrayExpression(validArguments), + ]); + + // Create a new variable declaration for the hook + const hookDeclaration = t.variableDeclaration('let', [t.variableDeclarator(hookId, createHookCall)]); + + // Insert the new declaration and hook updaters before the current statement + statementPath.insertBefore([hookDeclaration, ...updateWatchers]); + + // Replace the original call with hook.return(); + init.replaceWith(t.callExpression(t.memberExpression(hookId, t.identifier('value')), [])); + } + } + } + }); +} + +// Generate update watchers for each argument, which is not static +function generateUpdateWatchers( + hookId: t.Identifier, + validArguments: (babel.types.SpreadElement | babel.types.Expression)[] +) { + return validArguments + .filter(arg => !t.isLiteral(arg)) + .map((arg, idx) => { + const argName = `p${idx}`; + return t.expressionStatement( + t.callExpression(t.identifier('watch'), [ + t.arrowFunctionExpression( + [], + t.blockStatement([ + t.expressionStatement( + t.logicalExpression( + '&&', + wrapUntrack(hookId), + t.callExpression(t.memberExpression(wrapUntrack(hookId), t.identifier('updateProp'), false, true), [ + t.stringLiteral(argName), + arg, + ]) + ) + ), + ]) + ), + ]) + ); + }); +} 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 6476fa4314f11c34c2bf75977bb69d0c8a1798f7..48f23ea1a39ed60dd55e3551dd541493c1a1ef81 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts @@ -1,119 +1,119 @@ -/* - * 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 { types as t } from '@openinula/babel-api'; -import type { InulaNextOption } from '../types'; -import { register } from '@openinula/babel-api'; -import { importMap } from '../constants'; - -function transformJSXSlice(path: NodePath | NodePath) { - // don't handle the jsx in three cases: - // 1. return statement, like `return
    ` - // 2. arrow function, like `() =>
    ` - // 3. jsx as a children of other jsx, like `
    ` - if ( - path.parentPath.isReturnStatement() || - path.parentPath.isArrowFunctionExpression() || - path.parentPath.isJSXElement() || - path.parentPath.isJSXFragment() - ) { - // skip the children - return; - } - - const sliceCompNode = t.callExpression(t.identifier('Component'), [t.arrowFunctionExpression([], path.node)]); - // 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? $$Comp(Div$$) : $$Comp(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); - path.replaceWith(t.callExpression(t.identifier(importMap.Comp), [sliceId])); -} - -function genName(node: t.JSXElement | t.JSXFragment) { - if (t.isJSXFragment(node)) { - return 'Fragment'; - } - - 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; - } -} - -/** - * Analyze the JSX slice in the function component - * 1. VariableDeclaration, like `const a =
    ` - * 2. SubComponent, like `function Sub() { return
    }` - * - * i.e. - * ```jsx - * let jsxSlice =
    {count}
    - * // => - * function Comp_$id$() { - * return
    {count}
    - * } - * let jsxSlice = Comp_$id$() - * ``` - */ -export default function (api: typeof babel, options: InulaNextOption): PluginObj { - register(api); - return { - visitor: { - Program(program) { - program.traverse({ - JSXElement(path: NodePath) { - transformJSXSlice(path); - }, - JSXFragment(path: NodePath) { - transformJSXSlice(path); - }, - }); - }, - }, - }; -} +/* + * 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 { types as t } from '@openinula/babel-api'; +import type { InulaNextOption } from '../types'; +import { register } from '@openinula/babel-api'; +import { importMap } from '../constants'; + +function transformJSXSlice(path: NodePath | NodePath) { + // don't handle the jsx in three cases: + // 1. return statement, like `return
    ` + // 2. arrow function, like `() =>
    ` + // 3. jsx as a children of other jsx, like `
    ` + if ( + path.parentPath.isReturnStatement() || + path.parentPath.isArrowFunctionExpression() || + path.parentPath.isJSXElement() || + path.parentPath.isJSXFragment() + ) { + // skip the children + return; + } + + const sliceCompNode = t.callExpression(t.identifier('Component'), [t.arrowFunctionExpression([], path.node)]); + // 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? $$Comp(Div$$) : $$Comp(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); + path.replaceWith(t.callExpression(t.identifier(importMap.Comp), [sliceId])); +} + +function genName(node: t.JSXElement | t.JSXFragment) { + if (t.isJSXFragment(node)) { + return 'Fragment'; + } + + 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; + } +} + +/** + * Analyze the JSX slice in the function component + * 1. VariableDeclaration, like `const a =
    ` + * 2. SubComponent, like `function Sub() { return
    }` + * + * i.e. + * ```jsx + * let jsxSlice =
    {count}
    + * // => + * function Comp_$id$() { + * return
    {count}
    + * } + * let jsxSlice = Comp_$id$() + * ``` + */ +export default function (api: typeof babel, options: InulaNextOption): PluginObj { + register(api); + return { + visitor: { + Program(program) { + program.traverse({ + JSXElement(path: NodePath) { + transformJSXSlice(path); + }, + JSXFragment(path: NodePath) { + transformJSXSlice(path); + }, + }); + }, + }, + }; +} diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts index 8e2cb91d6fa55ffe97760b1d03f815d085088277..19bf9d11d03978fd5075608779130fe953f9b376 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts @@ -1,109 +1,109 @@ -/* - * 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 { register, types as t } from '@openinula/babel-api'; - -/** - * Mapping to for Plugin - * Convert map in JSXExpressionContainer to for - * arr.map((item) => (
    {item}
    )) -> {item =>
    {item}
    }
    - * Convert last map of multiple map call to for element - * arr.map(item =>

    {item}

    ).map((item) => (
    {item}
    )) ->

    {item}

    )}>{item =>
    {item}
    }
    - * Convert map of map to for in for - * {arr => {item =>
    {item}
    }
    }
    - * - * @param api Babel api - * @return PluginObj mapping to for plugin - */ - -export default function (api: typeof babel): PluginObj { - register(api); - return { - visitor: { - CallExpression(path: NodePath) { - callExpressionVisitor(path, false); - }, - }, - }; -} - -/** - * Convert map in JSXExpressionContainer to for visitor - * - * @param path Map call expression path - * @param inner is inside for tag - */ - -function callExpressionVisitor(path: NodePath, inner: boolean): void { - //match arrow function map call inside for tag - if (inner && !path.parentPath.isArrowFunctionExpression()) { - return; - } - //match map call in jsx expression container - if (!inner && !path.parentPath.isJSXExpressionContainer()) { - return; - } - - // don't convert map call inside for tag - if (path.parentPath?.parentPath?.parentPath?.isJSXOpeningElement()) { - return; - } - - const callee = path.get('callee'); - if (!callee.isMemberExpression()) { - return; - } - const object = callee.get('object'); - const map = callee.get('property'); - if (!map.isIdentifier()) { - return; - } - if (map.node.name !== 'map') { - return; - } - const mapArgs = path.get('arguments'); - if (mapArgs.length !== 1) { - return; - } - const expression = mapArgs[0]; - if (!expression.isExpression()) { - return; - } - - // generate for tag - const forElement = t.jsxElement( - t.jsxOpeningElement( - t.jSXIdentifier('for'), - [t.jsxAttribute(t.jsxIdentifier('each'), t.jsxExpressionContainer(object.node))], - false - ), - t.jsxClosingElement(t.jSXIdentifier('for')), - [t.jSXExpressionContainer(expression.node)] - ); - if (path.parentPath.isArrowFunctionExpression()) { - path.replaceWith(forElement); - } else { - path.parentPath.replaceWith(forElement); - } - // convert map call of arrow function inside for tag - if (!inner) { - path.parentPath.traverse({ - CallExpression(path: NodePath) { - callExpressionVisitor(path, true); - }, - }); - } -} +/* + * 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 { register, types as t } from '@openinula/babel-api'; + +/** + * Mapping to for Plugin + * Convert map in JSXExpressionContainer to for + * arr.map((item) => (
    {item}
    )) -> {item =>
    {item}
    }
    + * Convert last map of multiple map call to for element + * arr.map(item =>

    {item}

    ).map((item) => (
    {item}
    )) ->

    {item}

    )}>{item =>
    {item}
    }
    + * Convert map of map to for in for + * {arr => {item =>
    {item}
    }
    }
    + * + * @param api Babel api + * @return PluginObj mapping to for plugin + */ + +export default function (api: typeof babel): PluginObj { + register(api); + return { + visitor: { + CallExpression(path: NodePath) { + callExpressionVisitor(path, false); + }, + }, + }; +} + +/** + * Convert map in JSXExpressionContainer to for visitor + * + * @param path Map call expression path + * @param inner is inside for tag + */ + +function callExpressionVisitor(path: NodePath, inner: boolean): void { + //match arrow function map call inside for tag + if (inner && !path.parentPath.isArrowFunctionExpression()) { + return; + } + //match map call in jsx expression container + if (!inner && !path.parentPath.isJSXExpressionContainer()) { + return; + } + + // don't convert map call inside for tag + if (path.parentPath?.parentPath?.parentPath?.isJSXOpeningElement()) { + return; + } + + const callee = path.get('callee'); + if (!callee.isMemberExpression()) { + return; + } + const object = callee.get('object'); + const map = callee.get('property'); + if (!map.isIdentifier()) { + return; + } + if (map.node.name !== 'map') { + return; + } + const mapArgs = path.get('arguments'); + if (mapArgs.length !== 1) { + return; + } + const expression = mapArgs[0]; + if (!expression.isExpression()) { + return; + } + + // generate for tag + const forElement = t.jsxElement( + t.jsxOpeningElement( + t.jSXIdentifier('for'), + [t.jsxAttribute(t.jsxIdentifier('each'), t.jsxExpressionContainer(object.node))], + false + ), + t.jsxClosingElement(t.jSXIdentifier('for')), + [t.jSXExpressionContainer(expression.node)] + ); + if (path.parentPath.isArrowFunctionExpression()) { + path.replaceWith(forElement); + } else { + path.parentPath.replaceWith(forElement); + } + // convert map call of arrow function inside for tag + if (!inner) { + path.parentPath.traverse({ + CallExpression(path: NodePath) { + callExpressionVisitor(path, true); + }, + }); + } +} diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/propsFormatPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/propsFormatPlugin.ts index 262531c5d2d42ac88013b83ac8d6f3b49c9e9b6f..c0b31ec050a0239a8dc6e6780b61d11a04016fee 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/propsFormatPlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/propsFormatPlugin.ts @@ -1,279 +1,221 @@ -import babel, { NodePath, PluginObj } from '@babel/core'; -import type { InulaNextOption } from '../types'; -import { register, types as t } from '@openinula/babel-api'; -import { COMPONENT, PROP_SUFFIX } from '../constants'; -import { - ArrowFunctionWithBlock, - extractFnFromMacro, - isCompPath, - isHookPath, - wrapArrowFunctionWithBlock, -} from '../utils'; -import { type Scope } from '@babel/traverse'; - -export enum PropType { - REST = 'rest', - SINGLE = 'single', -} - -interface Prop { - name: string; - type: PropType; - alias?: string | null; - defaultVal?: t.Expression | null; - nestedRelationship?: t.ObjectPattern | t.ArrayPattern | null; -} - -// e.g. function({ prop1, prop2: [p20, p21] }) {} -// transform into -// function(prop1, prop2: [p20, p21]) { -// let prop1_$p$ = prop1 -// let prop2_$p$ = prop2 -// let [p20, p21] = prop2 // Attention: destructuring will be handled in state destructuring plugin. -// } -function createPropAssignment(prop: Prop, scope: Scope, suffix: string) { - const newName = `${prop.name}${suffix}`; - const declarations: t.Statement[] = [ - t.variableDeclaration('let', [t.variableDeclarator(t.identifier(newName), t.identifier(prop.name))]), - ]; - if (prop.alias) { - declarations.push( - t.variableDeclaration('let', [t.variableDeclarator(t.identifier(`${prop.alias}`), t.identifier(newName))]) - ); - } - if (prop.nestedRelationship) { - declarations.push( - t.variableDeclaration('let', [t.variableDeclarator(prop.nestedRelationship, t.identifier(newName))]) - ); - } - - if (!prop.nestedRelationship || !prop.alias) { - // this means the prop can be used directly in the function body - // need rename the prop - scope.rename(prop.name, newName); - } - return declarations; -} - -/** - * Find the props destructuring in the function body, like: - * const { prop1, prop2 } = props; - * To extract the props - * @param fnPath - * @param propsName - */ -function extractPropsDestructingInFnBody( - 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 => parseProperty(prop)); - } else if (id.isIdentifier()) { - props = extractPropsDestructingInFnBody(fnPath, id.node.name); - } - // delete the declaration - if (props.length > 0) { - path.remove(); - } - } - }); - }, - }); - return props; -} - -function extractMemberExpressionInFnBody( - fnPath: NodePath | NodePath, - propsName: string -) { - const props: Prop[] = []; - const body = fnPath.get('body') as NodePath; - body.traverse({ - MemberExpression(path: NodePath) { - // find the props destructuring, like const { prop1, prop2 } = props; - const obj = path.node.object; - - if (t.isIdentifier(obj) && obj.name === propsName) { - const property = path.node.property; - if (!t.isIdentifier(property)) { - return; - } - const name = property.name; - const prop: Prop = { type: PropType.SINGLE, name }; - if (path.parentPath.isVariableDeclarator() && t.isIdentifier(path.parentPath.node.id)) { - const rename = path.parentPath.node.id.name; - if (name !== rename) { - prop.alias = rename; - } - path.parentPath.remove(); - } - if (!props.map(prop => prop.name).includes(prop.name)) { - props.push(prop); - } - } - }, - }); - return props; -} - -function replaceProps( - fnPath: NodePath | NodePath, - propsName: string, - props: Prop[] -) { - const body = fnPath.get('body') as NodePath; - props.forEach(prop => { - body.traverse({ - MemberExpression(path: NodePath) { - const obj = path.node.object; - const property = path.node.property; - if (t.isIdentifier(property) && t.isIdentifier(obj) && obj.name === propsName && property.name === prop.name) { - path.replaceWith(t.identifier(prop.name + PROP_SUFFIX)); - } - }, - }); - }); -} - -function transformParam( - propsPath: NodePath, - props: Prop[], - fnPath: NodePath | NodePath, - suffix: string -) { - let memberExpressionProps: Prop[] = []; - if (propsPath) { - if (propsPath.isObjectPattern()) { - // --- object destructuring --- - props = propsPath.get('properties').map(prop => parseProperty(prop)); - } else if (propsPath.isIdentifier()) { - // --- identifier destructuring --- - props = extractPropsDestructingInFnBody(fnPath, propsPath.node.name); - memberExpressionProps = extractMemberExpressionInFnBody(fnPath, propsPath.node.name); - props = [...props, ...memberExpressionProps]; - } - } - - fnPath.node.body.body.unshift(...props.flatMap(prop => createPropAssignment(prop, fnPath.scope, suffix))); - if (propsPath.isIdentifier()) { - replaceProps(fnPath, propsPath.node.name, memberExpressionProps); - } - // --- only keep the first level props--- - // e.g. function({ prop1, prop2 }) {} - const param = t.objectPattern( - props.map(prop => (prop.type === PropType.REST ? t.restElement(t.identifier(prop.name)) : createProperty(prop))) - ); - fnPath.node.params = suffix === PROP_SUFFIX ? [param] : [fnPath.node.params[0], param]; -} - -/** - * 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: InulaNextOption): PluginObj { - register(api); - let props: Prop[]; - - return { - visitor: { - CallExpression(path: NodePath) { - if (isCompPath(path) || isHookPath(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]; - transformParam(propsPath, props, fnPath, PROP_SUFFIX); - } - }, - }, - }; -} - -function createProperty(prop: Prop) { - return t.objectProperty( - t.identifier(prop.name), - prop.defaultVal ? t.assignmentPattern(t.identifier(prop.name), prop.defaultVal) : t.identifier(prop.name), - false, - true - ); -} - -function parseProperty(path: NodePath): Prop { - if (path.isObjectProperty()) { - // --- normal property --- - const key = path.node.key; - if (t.isIdentifier(key) || t.isStringLiteral(key)) { - const name = t.isIdentifier(key) ? key.name : key.value; - return analyzeNestedProp(path.get('value'), name); - } - - 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(valuePath: NodePath, name: string): Prop { - const value = valuePath.node; - let defaultVal: t.Expression | null = null; - let alias: string | null = 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, nestedRelationship }; -} +import babel, { NodePath, PluginObj } from '@babel/core'; +import type { InulaNextOption } from '../types'; +import { register, types as t } from '@openinula/babel-api'; +import { COMPONENT, PROP_SUFFIX } from '../constants'; +import { + ArrowFunctionWithBlock, + extractFnFromMacro, + isCompPath, + isHookPath, + wrapArrowFunctionWithBlock, +} from '../utils'; +import { type Scope } from '@babel/traverse'; + +export enum PropType { + REST = 'rest', + SINGLE = 'single', +} + +interface Prop { + name: string; + type: PropType; + alias?: string | null; + defaultVal?: t.Expression | null; + nestedRelationship?: t.ObjectPattern | t.ArrayPattern | null; +} + +// e.g. function({ prop1, prop2: [p20, p21] }) {} +// transform into +// function(prop1, prop2: [p20, p21]) { +// let prop1_$p$ = prop1 +// let prop2_$p$ = prop2 +// let [p20, p21] = prop2 // Attention: destructuring will be handled in state destructuring plugin. +// } +function createPropAssignment(prop: Prop, scope: Scope, suffix: string) { + const newName = `${prop.name}${suffix}`; + const declarations: t.Statement[] = [ + t.variableDeclaration('let', [t.variableDeclarator(t.identifier(newName), t.identifier(prop.name))]), + ]; + if (prop.alias) { + declarations.push( + t.variableDeclaration('let', [t.variableDeclarator(t.identifier(`${prop.alias}`), t.identifier(newName))]) + ); + } + if (prop.nestedRelationship) { + declarations.push( + t.variableDeclaration('let', [t.variableDeclarator(prop.nestedRelationship, t.identifier(newName))]) + ); + } + + if (!prop.nestedRelationship || !prop.alias) { + // this means the prop can be used directly in the function body + // need rename the prop + scope.rename(prop.name, newName); + } + return declarations; +} + +/** + * Find the props destructuring in the function body, like: + * const { prop1, prop2 } = props; + * To extract the props + * @param fnPath + * @param propsName + */ +function extractPropsDestructingInFnBody( + 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 => parseProperty(prop)); + } else if (id.isIdentifier()) { + props = extractPropsDestructingInFnBody(fnPath, id.node.name); + } + // delete the declaration + if (props.length > 0) { + path.remove(); + } + } + }); + }, + }); + return props; +} + +function transformParam( + propsPath: NodePath, + props: Prop[], + fnPath: NodePath | NodePath, + suffix: string +) { + if (propsPath) { + if (propsPath.isObjectPattern()) { + // --- object destructuring --- + props = propsPath.get('properties').map(prop => parseProperty(prop)); + } else if (propsPath.isIdentifier()) { + // --- identifier destructuring --- + props = extractPropsDestructingInFnBody(fnPath, propsPath.node.name); + } + } + + fnPath.node.body.body.unshift(...props.flatMap(prop => createPropAssignment(prop, fnPath.scope, suffix))); + + // --- only keep the first level props--- + // e.g. function({ prop1, prop2 }) {} + const param = t.objectPattern( + props.map(prop => (prop.type === PropType.REST ? t.restElement(t.identifier(prop.name)) : createProperty(prop))) + ); + fnPath.node.params = suffix === PROP_SUFFIX ? [param] : [fnPath.node.params[0], param]; +} + +/** + * 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: InulaNextOption): PluginObj { + register(api); + let props: Prop[]; + + return { + visitor: { + CallExpression(path: NodePath) { + if (isCompPath(path) || isHookPath(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]; + transformParam(propsPath, props, fnPath, PROP_SUFFIX); + } + }, + }, + }; +} + +function createProperty(prop: Prop) { + return t.objectProperty( + t.identifier(prop.name), + prop.defaultVal ? t.assignmentPattern(t.identifier(prop.name), prop.defaultVal) : t.identifier(prop.name), + false, + true + ); +} + +function parseProperty(path: NodePath): Prop { + if (path.isObjectProperty()) { + // --- normal property --- + const key = path.node.key; + if (t.isIdentifier(key) || t.isStringLiteral(key)) { + const name = t.isIdentifier(key) ? key.name : key.value; + return analyzeNestedProp(path.get('value'), name); + } + + 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(valuePath: NodePath, name: string): Prop { + const value = valuePath.node; + let defaultVal: t.Expression | null = null; + let alias: string | null = 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, nestedRelationship }; +} diff --git a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/stateDestructuringPlugin.ts b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/stateDestructuringPlugin.ts index 8b0e92b6559a19fb72c2a8f81253d5cac9699028..38f6dec7e6e2c5c942be72dbfa38eca7836b1c22 100644 --- a/packages/transpiler/babel-inula-next-core/src/sugarPlugins/stateDestructuringPlugin.ts +++ b/packages/transpiler/babel-inula-next-core/src/sugarPlugins/stateDestructuringPlugin.ts @@ -1,121 +1,121 @@ -/* - * 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 { InulaNextOption } from '../types'; -import { register } from '@openinula/babel-api'; -import { COMPONENT } from '../constants'; -import { ArrowFunctionWithBlock, extractFnFromMacro, isCompPath, isHookPath } from '../utils'; -import { types as t } from '@openinula/babel-api'; - -/** - * Iterate identifier in nested destructuring, collect the identifier that can be used - * e.g. function ({prop1, prop2: [p20X, {p211, p212: p212X}]} - * we should collect prop1, p20X, p211, p212X - * @param idPath - */ -export function searchNestedProps(idPath: NodePath) { - const nestedProps: string[] | null = []; - - if (idPath.isObjectPattern() || idPath.isArrayPattern()) { - 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); - } - }, - }); - } - - return nestedProps; -} - -/** - * 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: InulaNextOption): PluginObj { - register(api); - return { - visitor: { - CallExpression(path: NodePath) { - if (!isCompPath(path) && !isHookPath(path)) { - return; - } - const fnPath = extractFnFromMacro(path, COMPONENT) as - | NodePath - | NodePath; - - fnPath.traverse({ - VariableDeclaration(declarationPath) { - const nodesToReplace: t.Statement[] = []; - - declarationPath.get('declarations').map(path => { - const idPath = path.get('id'); - const initNode = path.node.init; - - if (initNode && (idPath.isObjectPattern() || idPath.isArrayPattern())) { - const nestedProps: string[] | null = searchNestedProps(idPath); - if (nestedProps.length) { - // declare the nested props as the variable - nodesToReplace.push( - t.variableDeclaration( - 'let', - nestedProps.map(prop => t.variableDeclarator(t.identifier(prop))) - ) - ); - // move the deconstructing assignment into the watch function - nodesToReplace.push( - t.expressionStatement( - t.callExpression(t.identifier('watch'), [ - t.arrowFunctionExpression( - [], - t.blockStatement([t.expressionStatement(t.assignmentExpression('=', idPath.node, initNode))]) - ), - ]) - ) - ); - declarationPath.replaceWithMultiple(nodesToReplace); - } - } - }); - }, - }); - }, - }, - }; -} +/* + * 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 { InulaNextOption } from '../types'; +import { register } from '@openinula/babel-api'; +import { COMPONENT } from '../constants'; +import { ArrowFunctionWithBlock, extractFnFromMacro, isCompPath, isHookPath } from '../utils'; +import { types as t } from '@openinula/babel-api'; + +/** + * Iterate identifier in nested destructuring, collect the identifier that can be used + * e.g. function ({prop1, prop2: [p20X, {p211, p212: p212X}]} + * we should collect prop1, p20X, p211, p212X + * @param idPath + */ +export function searchNestedProps(idPath: NodePath) { + const nestedProps: string[] | null = []; + + if (idPath.isObjectPattern() || idPath.isArrayPattern()) { + 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); + } + }, + }); + } + + return nestedProps; +} + +/** + * 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: InulaNextOption): PluginObj { + register(api); + return { + visitor: { + CallExpression(path: NodePath) { + if (!isCompPath(path) && !isHookPath(path)) { + return; + } + const fnPath = extractFnFromMacro(path, COMPONENT) as + | NodePath + | NodePath; + + fnPath.traverse({ + VariableDeclaration(declarationPath) { + const nodesToReplace: t.Statement[] = []; + + declarationPath.get('declarations').map(path => { + const idPath = path.get('id'); + const initNode = path.node.init; + + if (initNode && (idPath.isObjectPattern() || idPath.isArrayPattern())) { + const nestedProps: string[] | null = searchNestedProps(idPath); + if (nestedProps.length) { + // declare the nested props as the variable + nodesToReplace.push( + t.variableDeclaration( + 'let', + nestedProps.map(prop => t.variableDeclarator(t.identifier(prop))) + ) + ); + // move the deconstructing assignment into the watch function + nodesToReplace.push( + t.expressionStatement( + t.callExpression(t.identifier('watch'), [ + t.arrowFunctionExpression( + [], + t.blockStatement([t.expressionStatement(t.assignmentExpression('=', idPath.node, initNode))]) + ), + ]) + ) + ); + declarationPath.replaceWithMultiple(nodesToReplace); + } + } + }); + }, + }); + }, + }, + }; +} diff --git a/packages/transpiler/babel-inula-next-core/src/types.ts b/packages/transpiler/babel-inula-next-core/src/types.ts index 07f3e625bb5debf8e6e0dbe1f57a765eeb249153..d0e7bc4249ceccbf24ad68e1a4059af2680cf2ba 100644 --- a/packages/transpiler/babel-inula-next-core/src/types.ts +++ b/packages/transpiler/babel-inula-next-core/src/types.ts @@ -1,61 +1,61 @@ -import { type types as t } from '@babel/core'; - -export type HTMLTags = string[] | ((defaultHtmlTags: string[]) => string[]); -export interface InulaNextOption { - /** - * Files that will be included - * @default ** /*.{js,jsx,ts,tsx} - */ - files?: string | string[]; - /** - * Files that will be excludes - * @default ** /{dist,node_modules,lib}/*.{js,ts} - */ - excludeFiles?: string | string[]; - /** - * Enable devtools - * @default false - */ - enableDevTools?: boolean; - /** - * Custom HTML tags. - * Accepts 2 types: - * 1. string[], e.g. ["div", "span"] - * if contains "*", then all default tags will be included - * 2. (defaultHtmlTags: string[]) => string[] - * @default defaultHtmlTags => defaultHtmlTags - */ - htmlTags?: HTMLTags; - /** - * Allowed HTML tags from attributes - * e.g. { alt: ["area", "img", "input"] } - */ - attributeMap?: Record; - /** - * The runtime package name that will be imported from - */ - packageName: string; - /** - * Skip importing the runtime package - */ - skipImport: boolean; -} - -export type PropertyContainer = Record< - string, - { - node: t.ClassProperty | t.ClassMethod; - deps: string[]; - isStatic?: boolean; - isContent?: boolean; - isChildren?: boolean | number; - isModel?: boolean; - isWatcher?: boolean; - isPropOrEnv?: 'Prop' | 'Env'; - depsNode?: t.ArrayExpression; - } ->; - -export type IdentifierToDepNode = t.SpreadElement | t.Expression; - -export type SnippetPropSubDepMap = Record>; +import { type types as t } from '@babel/core'; + +export type HTMLTags = string[] | ((defaultHtmlTags: string[]) => string[]); +export interface InulaNextOption { + /** + * Files that will be included + * @default ** /*.{js,jsx,ts,tsx} + */ + files?: string | string[]; + /** + * Files that will be excludes + * @default ** /{dist,node_modules,lib}/*.{js,ts} + */ + excludeFiles?: string | string[]; + /** + * Enable devtools + * @default false + */ + enableDevTools?: boolean; + /** + * Custom HTML tags. + * Accepts 2 types: + * 1. string[], e.g. ["div", "span"] + * if contains "*", then all default tags will be included + * 2. (defaultHtmlTags: string[]) => string[] + * @default defaultHtmlTags => defaultHtmlTags + */ + htmlTags?: HTMLTags; + /** + * Allowed HTML tags from attributes + * e.g. { alt: ["area", "img", "input"] } + */ + attributeMap?: Record; + /** + * The runtime package name that will be imported from + */ + packageName: string; + /** + * Skip importing the runtime package + */ + skipImport: boolean; +} + +export type PropertyContainer = Record< + string, + { + node: t.ClassProperty | t.ClassMethod; + deps: string[]; + isStatic?: boolean; + isContent?: boolean; + isChildren?: boolean | number; + isModel?: boolean; + isWatcher?: boolean; + isPropOrEnv?: 'Prop' | 'Env'; + depsNode?: t.ArrayExpression; + } +>; + +export type IdentifierToDepNode = t.SpreadElement | t.Expression; + +export type SnippetPropSubDepMap = Record>; diff --git a/packages/transpiler/babel-inula-next-core/src/utils.ts b/packages/transpiler/babel-inula-next-core/src/utils.ts index 18a2c0e36e9e3d15c0c84cad18ded7f345fa6db4..ab8fb85ea128f187a2f78a62ca41b7d3f4ccdcfb 100644 --- a/packages/transpiler/babel-inula-next-core/src/utils.ts +++ b/packages/transpiler/babel-inula-next-core/src/utils.ts @@ -1,123 +1,123 @@ -import { NodePath } from '@babel/core'; -import { types as t } from '@openinula/babel-api'; -import { COMPONENT, HOOK, importMap } from './constants'; -import { minimatch } from 'minimatch'; -import { SubComponentNode, Variable } from './analyze/types'; - -export function fileAllowed(fileName: string | undefined, includes: string[], excludes: string[]): boolean { - if (includes.includes('*')) return true; - if (!fileName) return false; - if (excludes.some(pattern => minimatch(fileName, pattern))) return false; - return includes.some(pattern => minimatch(fileName, pattern)); -} - -export function addImport(programNode: t.Program, importMap: Record, packageName: string) { - programNode!.body.unshift( - t.importDeclaration( - Object.entries(importMap).map(([key, value]) => t.importSpecifier(t.identifier(value), t.identifier(key))), - t.stringLiteral(packageName) - ) - ); -} - -export function toArray(val: T | T[]): T[] { - return Array.isArray(val) ? val : [val]; -} - -export function extractFnFromMacro( - path: NodePath, - macroName: string -): NodePath | NodePath { - const args = path.get('arguments'); - - const fnNode = args[0]; - if (fnNode.isFunctionExpression() || fnNode.isArrowFunctionExpression()) { - return fnNode; - } - - throw new Error(`${macroName} macro must have a function argument`); -} - -export function 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 - fnBody.replaceWith(t.blockStatement([t.returnStatement(fnBody.node)])); - } - - return fnBody as unknown as 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 function isHookPath(path: NodePath) { - // find the component, like: Component(() => {}) - const callee = path.get('callee'); - return callee.isIdentifier() && callee.node.name === HOOK; -} - -export function getMacroType(path: NodePath) { - if (isCompPath(path)) { - return COMPONENT; - } - if (isHookPath(path)) { - return HOOK; - } - - return null; -} - -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, - params: t.FunctionExpression['params'] = [] -) { - return t.callExpression(t.identifier(macroName), [t.arrowFunctionExpression(params, fnBody)]); -} - -export function isValidPath(path: NodePath): path is NodePath> { - return !!path.node; -} - -/** - * Wrap the expression with untrack - * e.g. untrack(() => a) - */ -export function wrapUntrack(node: t.Expression) { - return t.callExpression(t.identifier(importMap.untrack), [t.arrowFunctionExpression([], node)]); -} - -export function getSubComp(variables: Variable[]) { - return variables.filter((v): v is SubComponentNode => v.type === 'subComp'); -} +import { NodePath } from '@babel/core'; +import { types as t } from '@openinula/babel-api'; +import { COMPONENT, HOOK, importMap } from './constants'; +import { minimatch } from 'minimatch'; +import { SubComponentNode, Variable } from './analyze/types'; + +export function fileAllowed(fileName: string | undefined, includes: string[], excludes: string[]): boolean { + if (includes.includes('*')) return true; + if (!fileName) return false; + if (excludes.some(pattern => minimatch(fileName, pattern))) return false; + return includes.some(pattern => minimatch(fileName, pattern)); +} + +export function addImport(programNode: t.Program, importMap: Record, packageName: string) { + programNode!.body.unshift( + t.importDeclaration( + Object.entries(importMap).map(([key, value]) => t.importSpecifier(t.identifier(value), t.identifier(key))), + t.stringLiteral(packageName) + ) + ); +} + +export function toArray(val: T | T[]): T[] { + return Array.isArray(val) ? val : [val]; +} + +export function extractFnFromMacro( + path: NodePath, + macroName: string +): NodePath | NodePath { + const args = path.get('arguments'); + + const fnNode = args[0]; + if (fnNode.isFunctionExpression() || fnNode.isArrowFunctionExpression()) { + return fnNode; + } + + throw new Error(`${macroName} macro must have a function argument`); +} + +export function 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 + fnBody.replaceWith(t.blockStatement([t.returnStatement(fnBody.node)])); + } + + return fnBody as unknown as 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 function isHookPath(path: NodePath) { + // find the component, like: Component(() => {}) + const callee = path.get('callee'); + return callee.isIdentifier() && callee.node.name === HOOK; +} + +export function getMacroType(path: NodePath) { + if (isCompPath(path)) { + return COMPONENT; + } + if (isHookPath(path)) { + return HOOK; + } + + return null; +} + +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, + params: t.FunctionExpression['params'] = [] +) { + return t.callExpression(t.identifier(macroName), [t.arrowFunctionExpression(params, fnBody)]); +} + +export function isValidPath(path: NodePath): path is NodePath> { + return !!path.node; +} + +/** + * Wrap the expression with untrack + * e.g. untrack(() => a) + */ +export function wrapUntrack(node: t.Expression) { + return t.callExpression(t.identifier(importMap.untrack), [t.arrowFunctionExpression([], node)]); +} + +export function getSubComp(variables: Variable[]) { + return variables.filter((v): v is SubComponentNode => v.type === 'subComp'); +} diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/lifeCycle.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/lifeCycle.test.ts index 58473b8c211426127b7058b466576bc23776422c..ab3e11f1a984aa2fa98713c5544a52d7e7cefa2f 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/lifeCycle.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/lifeCycle.test.ts @@ -1,145 +1,145 @@ -/* - * 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 } from '../mock'; -import { functionalMacroAnalyze } from '../../src/analyze/Analyzers/functionalMacroAnalyze'; -import { types as t } from '@openinula/babel-api'; -import { mockAnalyze } from './mock'; - -const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]); -const combine = (body: t.Statement[]) => t.program(body); - -describe('analyze lifeCycle', () => { - it('should collect will mount', () => { - const root = analyze(/*js*/ ` - Component(() => { - willMount(() => { - console.log('test'); - }) - }) - `); - - expect(genCode(combine(root.lifecycle.willMount!))).toMatchInlineSnapshot(` - "{ - console.log('test'); - }" - `); - }); - - it('should collect on mount', () => { - const root = analyze(/*js*/ ` - Component(() => { - didMount(() => { - console.log('test'); - }) - }) - `); - - expect(genCode(combine(root.lifecycle.didMount!))).toMatchInlineSnapshot(` - "{ - console.log('test'); - }" - `); - }); - - it('should async on mount', () => { - const root = analyze(/*js*/ ` - Component(() => { - didMount(async () => { - const data = await fetch(API_URL) - }) - }) - `); - - expect(genCode(combine(root.lifecycle.didMount!))).toMatchInlineSnapshot(` - "(async () => { - const data = await fetch(API_URL); - })();" - `); - }); - - it('should collect willUnmount', () => { - const root = analyze(/*js*/ ` - Component(() => { - willUnmount(() => { - console.log('test'); - }) - }) - `); - - expect(genCode(combine(root.lifecycle.willUnmount!))).toMatchInlineSnapshot(` - "{ - console.log('test'); - }" - `); - }); - - it('should collect didUnmount', () => { - const root = analyze(/*js*/ ` - Component(() => { - didUnmount(() => { - console.log('test'); - }) - }) - `); - - expect(genCode(combine(root.lifecycle.didUnmount!))).toMatchInlineSnapshot(` - "{ - console.log('test'); - }" - `); - }); - - it('should handle multiple lifecycle methods', () => { - const root = analyze(/*js*/ ` - Component(() => { - willMount(() => { - console.log('willMount'); - }) - didMount(() => { - console.log('didMount'); - }) - willUnmount(() => { - console.log('willUnmount'); - }) - didUnmount(() => { - console.log('didUnmount'); - }) - }) - `); - - expect(genCode(combine(root.lifecycle.willMount!))).toMatchInlineSnapshot(` - "{ - console.log('willMount'); - }" - `); - expect(genCode(combine(root.lifecycle.didMount!))).toMatchInlineSnapshot(` - "{ - console.log('didMount'); - }" - `); - expect(genCode(combine(root.lifecycle.willUnmount!))).toMatchInlineSnapshot(` - "{ - console.log('willUnmount'); - }" - `); - expect(genCode(combine(root.lifecycle.didUnmount!))).toMatchInlineSnapshot(` - "{ - console.log('didUnmount'); - }" - `); - }); -}); +/* + * 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 } from '../mock'; +import { functionalMacroAnalyze } from '../../src/analyze/Analyzers/functionalMacroAnalyze'; +import { types as t } from '@openinula/babel-api'; +import { mockAnalyze } from './mock'; + +const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]); +const combine = (body: t.Statement[]) => t.program(body); + +describe('analyze lifeCycle', () => { + it('should collect will mount', () => { + const root = analyze(/*js*/ ` + Component(() => { + willMount(() => { + console.log('test'); + }) + }) + `); + + expect(genCode(combine(root.lifecycle.willMount!))).toMatchInlineSnapshot(` + "{ + console.log('test'); + }" + `); + }); + + it('should collect on mount', () => { + const root = analyze(/*js*/ ` + Component(() => { + didMount(() => { + console.log('test'); + }) + }) + `); + + expect(genCode(combine(root.lifecycle.didMount!))).toMatchInlineSnapshot(` + "{ + console.log('test'); + }" + `); + }); + + it('should async on mount', () => { + const root = analyze(/*js*/ ` + Component(() => { + didMount(async () => { + const data = await fetch(API_URL) + }) + }) + `); + + expect(genCode(combine(root.lifecycle.didMount!))).toMatchInlineSnapshot(` + "(async () => { + const data = await fetch(API_URL); + })();" + `); + }); + + it('should collect willUnmount', () => { + const root = analyze(/*js*/ ` + Component(() => { + willUnmount(() => { + console.log('test'); + }) + }) + `); + + expect(genCode(combine(root.lifecycle.willUnmount!))).toMatchInlineSnapshot(` + "{ + console.log('test'); + }" + `); + }); + + it('should collect didUnmount', () => { + const root = analyze(/*js*/ ` + Component(() => { + didUnmount(() => { + console.log('test'); + }) + }) + `); + + expect(genCode(combine(root.lifecycle.didUnmount!))).toMatchInlineSnapshot(` + "{ + console.log('test'); + }" + `); + }); + + it('should handle multiple lifecycle methods', () => { + const root = analyze(/*js*/ ` + Component(() => { + willMount(() => { + console.log('willMount'); + }) + didMount(() => { + console.log('didMount'); + }) + willUnmount(() => { + console.log('willUnmount'); + }) + didUnmount(() => { + console.log('didUnmount'); + }) + }) + `); + + expect(genCode(combine(root.lifecycle.willMount!))).toMatchInlineSnapshot(` + "{ + console.log('willMount'); + }" + `); + expect(genCode(combine(root.lifecycle.didMount!))).toMatchInlineSnapshot(` + "{ + console.log('didMount'); + }" + `); + expect(genCode(combine(root.lifecycle.willUnmount!))).toMatchInlineSnapshot(` + "{ + console.log('willUnmount'); + }" + `); + expect(genCode(combine(root.lifecycle.didUnmount!))).toMatchInlineSnapshot(` + "{ + console.log('didUnmount'); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/mock.ts b/packages/transpiler/babel-inula-next-core/test/analyze/mock.ts index 58b8d5e2656e28faae96ff029a44251c86a133ac..264aa513f7de25e22d336b76dffef9076e2477af 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/mock.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/mock.ts @@ -1,65 +1,65 @@ -/* - * 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 { Analyzer, ComponentNode } from '../../src/analyze/types'; -import { type PluginObj, transform as transformWithBabel } from '@babel/core'; -import syntaxJSX from '@babel/plugin-syntax-jsx'; -import { register } from '@openinula/babel-api'; -import { analyze } from '../../src/analyze'; -import { COMPONENT, defaultHTMLTags } from '../../src/constants'; - -export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode { - let root: ComponentNode | null = null; - transformWithBabel(code, { - plugins: [ - syntaxJSX.default ?? syntaxJSX, - function (api): PluginObj { - register(api); - const seen = new Set(); - return { - visitor: { - FunctionExpression: path => { - if (seen.has(path)) { - return; - } - root = analyze(COMPONENT, 'test', path, { customAnalyzers: analyzers, htmlTags: defaultHTMLTags }); - seen.add(path); - if (root) { - path.skip(); - } - }, - ArrowFunctionExpression: path => { - if (seen.has(path)) { - return; - } - root = analyze(COMPONENT, 'test', path, { customAnalyzers: analyzers, htmlTags: defaultHTMLTags }); - seen.add(path); - if (root) { - path.skip(); - } - }, - }, - }; - }, - ], - filename: 'test.tsx', - }); - - if (!root) { - throw new Error('root is null'); - } - - return root; -} +/* + * 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 { Analyzer, ComponentNode } from '../../src/analyze/types'; +import { type PluginObj, transform as transformWithBabel } from '@babel/core'; +import syntaxJSX from '@babel/plugin-syntax-jsx'; +import { register } from '@openinula/babel-api'; +import { analyze } from '../../src/analyze'; +import { COMPONENT, defaultHTMLTags } from '../../src/constants'; + +export function mockAnalyze(code: string, analyzers?: Analyzer[]): ComponentNode { + let root: ComponentNode | null = null; + transformWithBabel(code, { + plugins: [ + syntaxJSX.default ?? syntaxJSX, + function (api): PluginObj { + register(api); + const seen = new Set(); + return { + visitor: { + FunctionExpression: path => { + if (seen.has(path)) { + return; + } + root = analyze(COMPONENT, 'test', path, { customAnalyzers: analyzers, htmlTags: defaultHTMLTags }); + seen.add(path); + if (root) { + path.skip(); + } + }, + ArrowFunctionExpression: path => { + if (seen.has(path)) { + return; + } + root = analyze(COMPONENT, 'test', path, { customAnalyzers: analyzers, htmlTags: defaultHTMLTags }); + seen.add(path); + if (root) { + path.skip(); + } + }, + }, + }; + }, + ], + filename: 'test.tsx', + }); + + if (!root) { + throw new Error('root is null'); + } + + return root; +} diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts index 0ec7aefc6335ae1e385189712421ed1876f34d3d..a9f9868a605ea90d92a1b4c859e415bb8c7eb3a8 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts @@ -1,178 +1,178 @@ -/* - * 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 } from '../mock'; -import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze'; -import { ReactiveVariable, SubCompVariable } from '../../src/analyze/types'; -import { findVarByName } from './utils'; -import { viewAnalyze } from '../../src/analyze/Analyzers/viewAnalyze'; -import { mockAnalyze } from './mock'; - -const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze]); - -describe('analyze properties', () => { - it('should work', () => { - const root = analyze(` - Component(() => { - let foo = 1; - const bar = 1; - - return
    {foo}{bar}
    ; - }) - `); - expect(root.variables.length).toBe(2); - expect(findVarByName(root, 'foo').kind).toBe('let'); - }); - - describe('state dependency', () => { - it('should analyze dependency from state', () => { - const root = analyze(` - Component(() => { - let foo = 1; - let bar = foo; - let _ = bar; // use bar to avoid pruning - }) - `); - const fooVar = findVarByName(root, 'foo'); - expect(!!fooVar.dependency).toBe(false); - expect(genCode(fooVar.value)).toBe('1'); - - const barVar = findVarByName(root, 'bar'); - expect(!!barVar.dependency).toBe(true); - expect(genCode(barVar.value)).toBe('foo'); - expect(barVar.bit).toEqual(0b10); - expect(barVar.dependency!.depMask).toEqual(0b01); - }); - - it('should analyze dependency from state in different shape', () => { - const root = analyze(` - Component(() => { - let foo = 1; - let a = 1; - let b = 0; - let bar = { foo: foo ? a : b }; - let _ = bar; // use bar to avoid pruning - }) - `); - - const barVar = root.variables[3] as ReactiveVariable; - expect(!!barVar.dependency).toBe(true); - expect(genCode(barVar.value)).toMatchInlineSnapshot(` - "{ - foo: foo ? a : b - }" - `); - expect(barVar.dependency!.depMask).toEqual(0b0111); - }); - - // TODO:MOVE TO PROPS PLUGIN TEST - it.skip('should analyze dependency from props', () => { - const root = analyze(` - Component(({ foo }) => { - let bar = foo; - }) - `); - expect(root.variables.length).toBe(1); - - const barVar = root.variables[0] as ReactiveVariable; - expect(!!barVar.dependency).toBe(true); - }); - - // TODO:MOVE TO PROPS PLUGIN TEST - it.skip('should analyze dependency from nested props', () => { - const root = analyze(` - Component(({ foo: foo1, name: [first, last] }) => { - let bar = [foo1, first, last]; - }) - `); - expect(root.variables.length).toBe(1); - const barVar = root.variables[0] as ReactiveVariable; - expect(!!barVar.dependency).toBe(true); - // @ts-expect-error ignore ts here - expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] }); - }); - - it('should not collect invalid dependency', () => { - const root = analyze(` - const cond = true - Component(() => { - let bar = cond ? count : window.innerWidth; - let _ = bar; // use bar to avoid pruning - }) - `); - const barVar = root.variables[0] as ReactiveVariable; - expect(!!barVar.dependency).toBe(false); - expect(barVar.bit).toEqual(0b1); - }); - }); - - describe('subComponent', () => { - it('should analyze dependency from subComponent', () => { - const root = analyze(` - Component(() => { - let foo = 1; - const Sub = Component(() => { - let bar = foo; - let _ = bar; // use bar to avoid pruning - }); - }) - `); - expect((root.variables[1] as SubCompVariable).ownAvailableVariables[0].dependency!.depMask).toBe(0b1); - }); - - it('should analyze dependency in parent', () => { - const root = analyze(/*jsx*/ ` - Component(() => { - let lastName; - let parentFirstName = 'sheldon'; - const parentName = parentFirstName + lastName; - const Son = Component(() => { - let middleName = parentName - const name = 'shelly'+ middleName + lastName; - const GrandSon = Component(() => { - let grandSonName = name + lastName; - const _ = grandSonName; // use name to avoid pruning - }); - }); - }) - `); - const sonNode = root.variables[3] as SubCompVariable; - // Son > middleName - expect(findVarByName(sonNode, 'middleName').dependency!.depMask).toBe(0b100); - // Son > name - expect(findVarByName(sonNode, 'name').dependency!.depMask).toBe(0b1001); - const grandSonNode = sonNode.variables[2] as SubCompVariable; - // GrandSon > grandSonName - expect(grandSonNode.ownAvailableVariables[0].dependency!.depMask).toBe(0b10001); - }); - }); - - it('should collect method', () => { - const root = analyze(` - Component(() => { - let foo = 1; - const onClick = () => {}; - const onHover = function() { - onClick(foo) - }; - function onInput() {} - }) - `); - [1, 2, 3].forEach(i => { - expect(root.variables[i].type).toBe('plain'); - }); - }); -}); +/* + * 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 } from '../mock'; +import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze'; +import { ReactiveVariable, SubCompVariable } from '../../src/analyze/types'; +import { findVarByName } from './utils'; +import { viewAnalyze } from '../../src/analyze/Analyzers/viewAnalyze'; +import { mockAnalyze } from './mock'; + +const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze]); + +describe('analyze properties', () => { + it('should work', () => { + const root = analyze(` + Component(() => { + let foo = 1; + const bar = 1; + + return
    {foo}{bar}
    ; + }) + `); + expect(root.variables.length).toBe(2); + expect(findVarByName(root, 'foo').kind).toBe('let'); + }); + + describe('state dependency', () => { + it('should analyze dependency from state', () => { + const root = analyze(` + Component(() => { + let foo = 1; + let bar = foo; + let _ = bar; // use bar to avoid pruning + }) + `); + const fooVar = findVarByName(root, 'foo'); + expect(!!fooVar.dependency).toBe(false); + expect(genCode(fooVar.value)).toBe('1'); + + const barVar = findVarByName(root, 'bar'); + expect(!!barVar.dependency).toBe(true); + expect(genCode(barVar.value)).toBe('foo'); + expect(barVar.bit).toEqual(0b10); + expect(barVar.dependency!.depMask).toEqual(0b01); + }); + + it('should analyze dependency from state in different shape', () => { + const root = analyze(` + Component(() => { + let foo = 1; + let a = 1; + let b = 0; + let bar = { foo: foo ? a : b }; + let _ = bar; // use bar to avoid pruning + }) + `); + + const barVar = root.variables[3] as ReactiveVariable; + expect(!!barVar.dependency).toBe(true); + expect(genCode(barVar.value)).toMatchInlineSnapshot(` + "{ + foo: foo ? a : b + }" + `); + expect(barVar.dependency!.depMask).toEqual(0b0111); + }); + + // TODO:MOVE TO PROPS PLUGIN TEST + it.skip('should analyze dependency from props', () => { + const root = analyze(` + Component(({ foo }) => { + let bar = foo; + }) + `); + expect(root.variables.length).toBe(1); + + const barVar = root.variables[0] as ReactiveVariable; + expect(!!barVar.dependency).toBe(true); + }); + + // TODO:MOVE TO PROPS PLUGIN TEST + it.skip('should analyze dependency from nested props', () => { + const root = analyze(` + Component(({ foo: foo1, name: [first, last] }) => { + let bar = [foo1, first, last]; + }) + `); + expect(root.variables.length).toBe(1); + const barVar = root.variables[0] as ReactiveVariable; + expect(!!barVar.dependency).toBe(true); + // @ts-expect-error ignore ts here + expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] }); + }); + + it('should not collect invalid dependency', () => { + const root = analyze(` + const cond = true + Component(() => { + let bar = cond ? count : window.innerWidth; + let _ = bar; // use bar to avoid pruning + }) + `); + const barVar = root.variables[0] as ReactiveVariable; + expect(!!barVar.dependency).toBe(false); + expect(barVar.bit).toEqual(0b1); + }); + }); + + describe('subComponent', () => { + it('should analyze dependency from subComponent', () => { + const root = analyze(` + Component(() => { + let foo = 1; + const Sub = Component(() => { + let bar = foo; + let _ = bar; // use bar to avoid pruning + }); + }) + `); + expect((root.variables[1] as SubCompVariable).ownAvailableVariables[0].dependency!.depMask).toBe(0b1); + }); + + it('should analyze dependency in parent', () => { + const root = analyze(/*jsx*/ ` + Component(() => { + let lastName; + let parentFirstName = 'sheldon'; + const parentName = parentFirstName + lastName; + const Son = Component(() => { + let middleName = parentName + const name = 'shelly'+ middleName + lastName; + const GrandSon = Component(() => { + let grandSonName = name + lastName; + const _ = grandSonName; // use name to avoid pruning + }); + }); + }) + `); + const sonNode = root.variables[3] as SubCompVariable; + // Son > middleName + expect(findVarByName(sonNode, 'middleName').dependency!.depMask).toBe(0b100); + // Son > name + expect(findVarByName(sonNode, 'name').dependency!.depMask).toBe(0b1001); + const grandSonNode = sonNode.variables[2] as SubCompVariable; + // GrandSon > grandSonName + expect(grandSonNode.ownAvailableVariables[0].dependency!.depMask).toBe(0b10001); + }); + }); + + it('should collect method', () => { + const root = analyze(` + Component(() => { + let foo = 1; + const onClick = () => {}; + const onHover = function() { + onClick(foo) + }; + function onInput() {} + }) + `); + [1, 2, 3].forEach(i => { + expect(root.variables[i].type).toBe('plain'); + }); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts index 1b7711f3feb00a87a082c1de4a63fc30a59ce441..4985b03b993b4c7d616b3dd4d05274ff3013afb6 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts @@ -1,92 +1,92 @@ -import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze'; -import { viewAnalyze } from '../../src/analyze/Analyzers/viewAnalyze'; -import { genCode } from '../mock'; -import { describe, expect, it } from 'vitest'; -import { findSubCompByName } from './utils'; -import { mockAnalyze } from './mock'; - -const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze]); -describe('viewAnalyze', () => { - it('should analyze view', () => { - const root = analyze(/*js*/ ` - Component(({}) => { - let name; - let className; - let count = name; // 1 - let doubleCount = count * 2; // 2 - let doubleCount2 = doubleCount * 2; // 4 - const Input = Component(() => { - let count = 1; - return {count}{doubleCount}; - }); - return
    {doubleCount2}
    ; - }); - `); - const div = root.children![0] as any; - expect(div.children[0].content.depMask).toEqual(0b10000); - expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount2]"'); - expect(div.props.className.depMask).toEqual(0b110); - expect(genCode(div.props.className.value)).toMatchInlineSnapshot('"className + count"'); - - const InputCompNode = findSubCompByName(root, 'Input'); - // @ts-expect-error ignore ts here - // it's the {count} - const inputFirstExp = InputCompNode.children![0].children[0]; - expect(inputFirstExp.content.depMask).toEqual(0b100000); - expect(genCode(inputFirstExp.content.dependenciesNode)).toMatchInlineSnapshot('"[count]"'); - - // @ts-expect-error ignore ts here - // it's the {doubleCount} - const inputSecondExp = InputCompNode.children[0].children[1]; - expect(inputSecondExp.content.depMask).toEqual(0b1000); - expect(genCode(inputSecondExp.content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount]"'); - }); - - it('should analyze object state', () => { - const root = analyze(/*js*/ ` - Component(({}) => { - const info = { - firstName: 'John', - lastName: 'Doe' - } - return

    {info.firstName}

    ; - }); - `); - const div = root.children![0] as any; - expect(div.children[0].content.depMask).toEqual(0b1); - expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot(`"[info?.firstName]"`); - }); - - it('should analyze for loop', () => { - const root = analyze(/*js*/ ` - Component(({}) => { - const unused = 0; - const prefix = 'test'; - const list = [{name: 1}, {name: 2}, {name: 3}]; - const list2 = [{name: 4}, {name: 5}, {name: 6}]; - return { - ({name}) =>
    {prefix + name}
    - }
    ; - }); - `); - const forNode = root.children![0] as any; - expect(forNode.children[0].children[0].content.depMask).toEqual(0b11); - expect(genCode(forNode.children[0].children[0].content.dependenciesNode)).toMatchInlineSnapshot(`"[name]"`); - }); - - it('should collect key for loop', () => { - const root = analyze(/*js*/ ` - Component(({}) => { - const unused = 0; - const prefix = 'test'; - const list = [{name: 1}, {name: 2}, {name: 3}]; - const list2 = [{name: 4}, {name: 5}, {name: 6}]; - return { - ({name}) =>
    {prefix + name}
    - }
    ; - }); - `); - const forNode = root.children![0] as any; - expect(genCode(forNode.key)).toMatchInlineSnapshot(`"name"`); - }); -}); +import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze'; +import { viewAnalyze } from '../../src/analyze/Analyzers/viewAnalyze'; +import { genCode } from '../mock'; +import { describe, expect, it } from 'vitest'; +import { findSubCompByName } from './utils'; +import { mockAnalyze } from './mock'; + +const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze]); +describe('viewAnalyze', () => { + it('should analyze view', () => { + const root = analyze(/*js*/ ` + Component(({}) => { + let name; + let className; + let count = name; // 1 + let doubleCount = count * 2; // 2 + let doubleCount2 = doubleCount * 2; // 4 + const Input = Component(() => { + let count = 1; + return {count}{doubleCount}; + }); + return
    {doubleCount2}
    ; + }); + `); + const div = root.children![0] as any; + expect(div.children[0].content.depMask).toEqual(0b10000); + expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount2]"'); + expect(div.props.className.depMask).toEqual(0b110); + expect(genCode(div.props.className.value)).toMatchInlineSnapshot('"className + count"'); + + const InputCompNode = findSubCompByName(root, 'Input'); + // @ts-expect-error ignore ts here + // it's the {count} + const inputFirstExp = InputCompNode.children![0].children[0]; + expect(inputFirstExp.content.depMask).toEqual(0b100000); + expect(genCode(inputFirstExp.content.dependenciesNode)).toMatchInlineSnapshot('"[count]"'); + + // @ts-expect-error ignore ts here + // it's the {doubleCount} + const inputSecondExp = InputCompNode.children[0].children[1]; + expect(inputSecondExp.content.depMask).toEqual(0b1000); + expect(genCode(inputSecondExp.content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount]"'); + }); + + it('should analyze object state', () => { + const root = analyze(/*js*/ ` + Component(({}) => { + const info = { + firstName: 'John', + lastName: 'Doe' + } + return

    {info.firstName}

    ; + }); + `); + const div = root.children![0] as any; + expect(div.children[0].content.depMask).toEqual(0b1); + expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot(`"[info?.firstName]"`); + }); + + it('should analyze for loop', () => { + const root = analyze(/*js*/ ` + Component(({}) => { + const unused = 0; + const prefix = 'test'; + const list = [{name: 1}, {name: 2}, {name: 3}]; + const list2 = [{name: 4}, {name: 5}, {name: 6}]; + return { + ({name}) =>
    {prefix + name}
    + }
    ; + }); + `); + const forNode = root.children![0] as any; + expect(forNode.children[0].children[0].content.depMask).toEqual(0b11); + expect(genCode(forNode.children[0].children[0].content.dependenciesNode)).toMatchInlineSnapshot(`"[name]"`); + }); + + it('should collect key for loop', () => { + const root = analyze(/*js*/ ` + Component(({}) => { + const unused = 0; + const prefix = 'test'; + const list = [{name: 1}, {name: 2}, {name: 3}]; + const list2 = [{name: 4}, {name: 5}, {name: 6}]; + return { + ({name}) =>
    {prefix + name}
    + }
    ; + }); + `); + const forNode = root.children![0] as any; + expect(genCode(forNode.key)).toMatchInlineSnapshot(`"name"`); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts index 9da15e9af70bf46d5da8c543ab14fd166c507cb2..51d48ac952b3b010a28ca41ac840ce77145f3f2d 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts @@ -1,82 +1,82 @@ -import { functionalMacroAnalyze } from '../../src/analyze/Analyzers/functionalMacroAnalyze'; -import { genCode } from '../mock'; -import { describe, expect, it } from 'vitest'; -import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze'; -import { mockAnalyze } from './mock'; -import { findVarByName } from './utils'; - -const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze, variablesAnalyze]); - -describe('watchAnalyze', () => { - it('should analyze watch expressions', () => { - const root = analyze(/*js*/ ` - Comp(() => { - let a = 0; - let b = 0; - watch(() => { - console.log(a, b); - }); - }) - `); - expect(root.watch).toHaveLength(1); - if (!root?.watch?.[0].callback) { - throw new Error('watch callback not found'); - } - expect(genCode(root.watch[0].callback.node)).toMatchInlineSnapshot(` - "() => { - console.log(a, b); - }" - `); - if (!root.watch[0].dependency) { - throw new Error('watch deps not found'); - } - expect(root.watch[0].dependency.depMask).toBe(0b11); - }); - - it('should support untrack', () => { - const root = analyze(/*js*/ ` - Comp(() => { - let a = 0; - let b = 0; - watch(() => { - console.log(untrack(() => a), b); - }) - }) - `); - if (!root?.watch?.[0].callback) { - throw new Error('watch callback not found'); - } - if (!root.watch[0].dependency) { - throw new Error('watch deps not found'); - } - // a is untrack, so it should not be tracked - expect(findVarByName(root, 'a').bit).toBe(0); - expect(findVarByName(root, 'b').bit).toBe(1); - expect(root.watch[0].dependency.depMask).toBe(0b1); - }); - - it('should analyze watch expressions with dependency array', () => { - const root = analyze(/*js*/ ` - Comp(() => { - let a = 0; - let b = 0; - watch(() => { - // watch expression - }, [a, b]); - }) - `); - expect(root.watch).toHaveLength(1); - if (!root?.watch?.[0].callback) { - throw new Error('watch callback not found'); - } - expect(genCode(root.watch[0].callback.node)).toMatchInlineSnapshot(` - "() => { - // watch expression - }" - `); - if (!root.watch[0].dependency) { - throw new Error('watch deps not found'); - } - expect(root.watch[0].dependency.depMask).toBe(0b11); - }); -}); +import { functionalMacroAnalyze } from '../../src/analyze/Analyzers/functionalMacroAnalyze'; +import { genCode } from '../mock'; +import { describe, expect, it } from 'vitest'; +import { variablesAnalyze } from '../../src/analyze/Analyzers/variablesAnalyze'; +import { mockAnalyze } from './mock'; +import { findVarByName } from './utils'; + +const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze, variablesAnalyze]); + +describe('watchAnalyze', () => { + it('should analyze watch expressions', () => { + const root = analyze(/*js*/ ` + Comp(() => { + let a = 0; + let b = 0; + watch(() => { + console.log(a, b); + }); + }) + `); + expect(root.watch).toHaveLength(1); + if (!root?.watch?.[0].callback) { + throw new Error('watch callback not found'); + } + expect(genCode(root.watch[0].callback.node)).toMatchInlineSnapshot(` + "() => { + console.log(a, b); + }" + `); + if (!root.watch[0].dependency) { + throw new Error('watch deps not found'); + } + expect(root.watch[0].dependency.depMask).toBe(0b11); + }); + + it('should support untrack', () => { + const root = analyze(/*js*/ ` + Comp(() => { + let a = 0; + let b = 0; + watch(() => { + console.log(untrack(() => a), b); + }) + }) + `); + if (!root?.watch?.[0].callback) { + throw new Error('watch callback not found'); + } + if (!root.watch[0].dependency) { + throw new Error('watch deps not found'); + } + // a is untrack, so it should not be tracked + expect(findVarByName(root, 'a').bit).toBe(0); + expect(findVarByName(root, 'b').bit).toBe(1); + expect(root.watch[0].dependency.depMask).toBe(0b1); + }); + + it('should analyze watch expressions with dependency array', () => { + const root = analyze(/*js*/ ` + Comp(() => { + let a = 0; + let b = 0; + watch(() => { + // watch expression + }, [a, b]); + }) + `); + expect(root.watch).toHaveLength(1); + if (!root?.watch?.[0].callback) { + throw new Error('watch callback not found'); + } + expect(genCode(root.watch[0].callback.node)).toMatchInlineSnapshot(` + "() => { + // watch expression + }" + `); + if (!root.watch[0].dependency) { + throw new Error('watch deps not found'); + } + expect(root.watch[0].dependency.depMask).toBe(0b11); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/e2e/children.test.tsx b/packages/transpiler/babel-inula-next-core/test/e2e/children.test.tsx index f9cd0edfe638b89bf4a946f914fcdd8a0e14c0a1..eaa30b2399d52c63de8d43a21b452a5ce99b1c05 100644 --- a/packages/transpiler/babel-inula-next-core/test/e2e/children.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/e2e/children.test.tsx @@ -1,110 +1,110 @@ -/* - * 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. - */ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { transform } from '../mock'; - -describe('children', () => { - it('should support children', () => { - expect( - transform(` - function App() { - return ; - } - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, Comp as $$Comp } from "@openinula/next"; - function App() { - let self; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0; - $node0 = $$Comp(Child, { - "name": 'hello world!!!' - }); - return [[$node0],,]; - } - }); - return self.init(); - }" - `); - }); - it('should support children in comp', () => { - expect( - transform(` - function MyComp() { - let count = 0; - const add = () => (count += 1); - return ( - <> - -

    {count}

    -
    - - ); - } - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, createElement as $$createElement, ExpNode as $$ExpNode, insertNode as $$insertNode, PropView as $$PropView, Comp as $$Comp } from "@openinula/next"; - function MyComp() { - let self; - let count = 0; - const add = () => self.updateDerived(count += 1, 1 /*0b1*/); - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1; - $node1 = new $$PropView($addUpdate => { - let $node0, $node1; - $addUpdate($changed => { - if ($changed & 1) { - $node1 && $node1.update(() => count, [count]); - } - }); - $node0 = $$createElement("h1"); - $node1 = new $$ExpNode(count, [count]); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [$node0]; - }); - $node0 = $$Comp(Sub, { - "children": $node1 - }); - return [[$node0], $changed => { - $node1 && $node1.update($changed); - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); -}); +/* + * 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. + */ +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from '../mock'; + +describe('children', () => { + it('should support children', () => { + expect( + transform(` + function App() { + return ; + } + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, Comp as $$Comp, setProp as $$setProp, initCompNode as $$initCompNode } from "@openinula/next"; + function App() { + let self; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0; + $node0 = $$Comp(Child, { + "name": 'hello world!!!' + }); + return [[$node0],,]; + } + }); + return $$initCompNode(self); + }" + `); + }); + it('should support children in comp', () => { + expect( + transform(` + function MyComp() { + let count = 0; + const add = () => (count += 1); + return ( + <> + +

    {count}

    +
    + + ); + } + `) + ).toMatchInlineSnapshot(` + "import { updateNode as $$updateNode, createComponent as $$createComponent, createElement as $$createElement, createNode as $$createNode, insertNode as $$insertNode, Comp as $$Comp, initCompNode as $$initCompNode } from "@openinula/next"; + function MyComp() { + let self; + let count = 0; + const add = () => $$updateNode(self, count += 1, 1 /*0b1*/); + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1; + $node1 = $$createNode(6 /*Children*/, $addUpdate => { + let $node0, $node1; + $addUpdate($changed => { + if ($changed & 1) { + $node1 && $$updateNode($node1, () => count, [count]); + } + }); + $node0 = $$createElement("h1"); + $node1 = $$createNode(3 /*Exp*/, () => count, [count]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [$node0]; + }); + $node0 = $$Comp(Sub, { + "children": $node1 + }); + return [[$node0], $changed => { + $$updateNode($node1, $changed); + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/e2e/context.test.tsx b/packages/transpiler/babel-inula-next-core/test/e2e/context.test.tsx index 7ea291a02b849900feb0e1e47b927d985d7982ba..402981bffc56ebca734195716ad12d58b67d74e1 100644 --- a/packages/transpiler/babel-inula-next-core/test/e2e/context.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/e2e/context.test.tsx @@ -1,114 +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 { transform } from '../mock'; - -describe('context', () => { - describe('consumer', () => { - it('should generate update function for single context with specific key', () => { - expect( - transform(` - function App() { - const { level, path } = useContext(UserContext); - console.log(level, path); - - return
    {level}-{path}
    ; - } - `) - ).toMatchInlineSnapshot(` - "import { Comp as $$Comp, createComponent as $$createComponent, createElement as $$createElement, ExpNode as $$ExpNode, insertNode as $$insertNode, createTextNode as $$createTextNode } from "@openinula/next"; - function App() { - let self; - let path_$c$_ = useContext(UserContext, "path"); - let level_$c$_ = useContext(UserContext, "level"); - self = $$createComponent({ - updateState: changed => {}, - updateContext: (ctx, key, value) => { - if (ctx === UserContext) { - if (key === "path") { - self.updateDerived(path_$c$_ = value, 1 /*0b1*/); - } - if (key === "level") { - self.updateDerived(level_$c$_ = value, 2 /*0b10*/); - } - } - }, - getUpdateViews: () => { - console.log(level_$c$_, path_$c$_); - let $node0, $node1, $node2, $node3; - $node0 = $$createElement("div"); - $node1 = new $$ExpNode(level_$c$_, [level_$c$_]); - $$insertNode($node0, $node1, 0); - $node2 = $$createTextNode("-", []); - $$insertNode($node0, $node2, 1); - $node3 = new $$ExpNode(path_$c$_, [path_$c$_]); - $$insertNode($node0, $node3, 2); - $node0._$nodes = [$node1, $node2, $node3]; - return [[$node0], $changed => { - if ($changed & 1) { - $node3 && $node3.update(() => path_$c$_, [path_$c$_]); - } - if ($changed & 2) { - $node1 && $node1.update(() => level_$c$_, [level_$c$_]); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); - }); - - describe('provider', () => { - it('should provide specific key', () => { - expect( - transform(` - function App() { - let level; - return - } - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, ContextProvider as $$ContextProvider } from "@openinula/next"; - function App() { - let self; - let level; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0; - $node0 = new $$ContextProvider(FileContext, { - level: level - }, { - level: [level] - }); - $node0.initNodes([]); - return [[$node0], $changed => { - if ($changed & 1) { - $node0 && $node0.updateContext("level", () => level, [level]); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from '../mock'; + +describe('context', () => { + describe('consumer', () => { + it('should generate update function for single context with specific key', () => { + expect( + transform(` + function App() { + const { level, path } = useContext(UserContext); + console.log(level, path); + + return
    {level}-{path}
    ; + } + `) + ).toMatchInlineSnapshot(` + "import { Comp as $$Comp, createComponent as $$createComponent, updateNode as $$updateNode, createElement as $$createElement, createNode as $$createNode, insertNode as $$insertNode, createTextNode as $$createTextNode, appendNode as $$appendNode, initCompNode as $$initCompNode } from "@openinula/next"; + function App() { + let self; + let path_$c$_ = useContext(UserContext, "path"); + let level_$c$_ = useContext(UserContext, "level"); + self = $$createComponent({ + updateState: changed => {}, + updateContext: (ctx, key, value) => { + if (ctx === UserContext) { + if (key === "path") { + $$updateNode(self, path_$c$_ = value, 1 /*0b1*/); + } + if (key === "level") { + $$updateNode(self, level_$c$_ = value, 2 /*0b10*/); + } + } + }, + getUpdateViews: () => { + console.log(level_$c$_, path_$c$_); + let $node0, $node1, $node2, $node3; + $node0 = $$createElement("div"); + $node1 = $$createNode(3 /*Exp*/, () => level_$c$_, [level_$c$_]); + $$insertNode($node0, $node1, 0); + $node2 = $$createTextNode("-", []); + $$appendNode($node0, $node2); + $node3 = $$createNode(3 /*Exp*/, () => path_$c$_, [path_$c$_]); + $$insertNode($node0, $node3, 2); + $node0._$nodes = [$node1, $node2, $node3]; + return [[$node0], $changed => { + if ($changed & 1) { + $node3 && $$updateNode($node3, () => path_$c$_, [path_$c$_]); + } + if ($changed & 2) { + $node1 && $$updateNode($node1, () => level_$c$_, [level_$c$_]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + }); + + describe('provider', () => { + it('should provide specific key', () => { + expect( + transform(` + function App() { + let level; + return + } + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, createNode as $$createNode, initContextChildren as $$initContextChildren, updateNode as $$updateNode, initCompNode as $$initCompNode } from "@openinula/next"; + function App() { + let self; + let level; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0; + $node0 = $$createNode(5 /*Context*/, FileContext, { + level: level + }, { + level: [level] + }); + $$initContextChildren($node0, []); + return [[$node0], $changed => { + if ($changed & 1) { + $node0 && $$updateNode($node0, "level", () => level, [level]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/e2e/forSubComponent.test.tsx b/packages/transpiler/babel-inula-next-core/test/e2e/forSubComponent.test.tsx index 058fdec00b78ad8795a026ef49724789da628502..01287a96685b9ea3e616cd14e27e340adfc8abb0 100644 --- a/packages/transpiler/babel-inula-next-core/test/e2e/forSubComponent.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/e2e/forSubComponent.test.tsx @@ -24,29 +24,31 @@ describe('for sub component', () => { let arr = [{x:1, y:1}, {x:2, y:2}, {x:3, y:3}] return (
    - {({x, y}, index) => { - let name1 = 'test' - const onClick = () => { - name1 = 'test2' - } + {({x, y}, index) => { + let name1 = 'test' + const onClick = () => { + name1 = 'test2' + } - return
    {name1}
    - }}
    - {({x, y}, index) => { - let name1 = 'test' - const onClick = () => { - name1 = 'test2' - } + return
    {name1}
    + }} +
    + {({x, y}, index) => { + let name1 = 'test' + const onClick = () => { + name1 = 'test2' + } - return
    {name1}
    - }}
    + return
    {name1}
    + }} +
    ) } `; const transformedCode = transform(code); expect(transformedCode).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, createElement as $$createElement, setHTMLProp as $$setHTMLProp, delegateEvent as $$delegateEvent, setStyle as $$setStyle, ExpNode as $$ExpNode, insertNode as $$insertNode, Comp as $$Comp, ForNode as $$ForNode } from "@openinula/next"; + "import { updateNode as $$updateNode, createComponent as $$createComponent, createElement as $$createElement, setHTMLProp as $$setHTMLProp, delegateEvent as $$delegateEvent, setStyle as $$setStyle, createNode as $$createNode, insertNode as $$insertNode, initCompNode as $$initCompNode, Comp as $$Comp, setProp as $$setProp, updateChildren as $$updateChildren } from "@openinula/next"; function MyComp() { let self; let name = 'test'; @@ -60,7 +62,7 @@ describe('for sub component', () => { x: 3, y: 3 }]; - function Comp_$id$({ + function For_1({ x, y, index @@ -71,13 +73,13 @@ describe('for sub component', () => { let index_$p$_ = index; let name1 = 'test'; const onClick = () => { - self1.updateDerived(self.updateDerived(name1 = 'test2', 2 /*0b10*/), 8 /*0b1000*/); + $$updateNode(self1, name1 = 'test2', 8 /*0b1000*/); }; self1 = $$createComponent({ updateState: changed => {}, updateProp: (propName, newValue) => { if (propName === "index") { - self1.updateDerived(index_$p$_ = newValue, 4 /*0b100*/); + $$updateNode(self1, index_$p$_ = newValue, 4 /*0b100*/); } else if (propName === "y") { y_$p$_ = newValue; } else if (propName === "x") { @@ -92,7 +94,7 @@ describe('for sub component', () => { $$setStyle($node0, { x: index_$p$_ }); - $node1 = new $$ExpNode(name1, [name1]); + $node1 = $$createNode(3 /*Exp*/, () => name1, [name1]); $$insertNode($node0, $node1, 0); $node0._$nodes = [$node1]; return [[$node0], $changed => { @@ -105,19 +107,99 @@ describe('for sub component', () => { }); } if ($changed & 8) { - $node1 && $node1.update(() => name1, [name1]); + $node1 && $$updateNode($node1, () => name1, [name1]); } return [$node0]; }]; } }); - return self1.init(); + return $$initCompNode(self1); + } + function For_2({ + x, + y, + index + }) { + let self1; + let x_$p$_ = x; + let y_$p$_ = y; + let index_$p$_ = index; + let name1 = 'test'; + const onClick = () => { + $$updateNode(self1, name1 = 'test2', 8 /*0b1000*/); + }; + self1 = $$createComponent({ + updateState: changed => {}, + updateProp: (propName, newValue) => { + if (propName === "index") { + $$updateNode(self1, index_$p$_ = newValue, 4 /*0b100*/); + } else if (propName === "y") { + y_$p$_ = newValue; + } else if (propName === "x") { + x_$p$_ = newValue; + } + }, + getUpdateViews: () => { + let $node0, $node1; + $node0 = $$createElement("div"); + $$setHTMLProp($node0, "className", () => name, [name]); + $$delegateEvent($node0, "click", onClick); + $$setStyle($node0, { + x: index_$p$_ + }); + $node1 = $$createNode(3 /*Exp*/, () => name1, [name1]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [[$node0], $changed => { + if ($changed & 1) { + $node0 && $$setHTMLProp($node0, "className", () => name, [name]); + } + if ($changed & 4) { + $node0 && $$setStyle($node0, { + x: index_$p$_ + }); + } + if ($changed & 8) { + $node1 && $$updateNode($node1, () => name1, [name1]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self1); } self = $$createComponent({ updateState: changed => {}, getUpdateViews: () => { - let $node0; - $node0 = new $$ForNode(arr, 2, null, ({ + let $node0, $node1, $node2; + $node0 = $$createElement("div"); + $node1 = $$createNode(1 /*For*/, arr, 2, null, ({ + x, + y + }, index, $updateArr) => { + let $node0; + $updateArr[index] = ($changed, $item) => { + ({ + x, + y + } = $item); + if ($changed & 2) { + $node0 && $$setProp($node0, "x", () => x, [x]); + $node0 && $$setProp($node0, "y", () => y, [y]); + } + if ($changed & 49) { + $$updateNode($node0, null, 49); + } + }; + $node0 = $$Comp(For_1, { + "x": x, + "y": y, + "index": index + }); + return [$node0]; + }); + $$insertNode($node0, $node1, 0); + $node2 = $$createNode(1 /*For*/, arr, 2, null, ({ x, y }, index, $updateArr) => { @@ -128,30 +210,34 @@ describe('for sub component', () => { y } = $item); if ($changed & 2) { - $node0 && $node0._$setProp("x", () => x, [x]); - $node0 && $node0._$setProp("y", () => y, [y]); + $node0 && $$setProp($node0, "x", () => x, [x]); + $node0 && $$setProp($node0, "y", () => y, [y]); } if ($changed & 49) { - $node0.updateDerived(null, 49); + $$updateNode($node0, null, 49); } }; - $node0 = $$Comp(Comp_$id$, { + $node0 = $$Comp(For_2, { "x": x, "y": y, "index": index }); return [$node0]; }); + $$insertNode($node0, $node2, 1); + $node0._$nodes = [$node1, $node2]; return [[$node0], $changed => { if ($changed & 2) { - $node0 && $node0.updateArray(arr, null); + $node1 && $$updateNode($node1, arr, null); + $node2 && $$updateNode($node2, arr, null); } - $node0 && $node0.update($changed); + $node1 && $$updateChildren($node1, $changed); + $node2 && $$updateChildren($node2, $changed); return [$node0]; }]; } }); - return self.init(); + return $$initCompNode(self); }" `); }); diff --git a/packages/transpiler/babel-inula-next-core/test/e2e/fragment.test.tsx b/packages/transpiler/babel-inula-next-core/test/e2e/fragment.test.tsx index eda2a1c9111c91cfb52db9d202790334f5e8d51c..61dfb7d08b5da5a68f1d8ae8f4d637cdd34253e5 100644 --- a/packages/transpiler/babel-inula-next-core/test/e2e/fragment.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/e2e/fragment.test.tsx @@ -1,52 +1,52 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { transform } from '../mock'; - -describe('fragment', () => { - it('should support elements in fragment', () => { - expect( - transform(` - import { render } from '@openinula/next'; - - function App () { - return <>
    xxx
    - } - render( - App, - document.getElementById('app') - ); - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, createElement as $$createElement } from "@openinula/next"; - import { render } from '@openinula/next'; - function App() { - let self; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0; - $node0 = $$createElement("div"); - $node0.textContent = "xxx"; - return [[$node0],,]; - } - }); - return self.init(); - } - render(App, document.getElementById('app'));" - `); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from '../mock'; + +describe('fragment', () => { + it('should support elements in fragment', () => { + expect( + transform(` + import { render } from '@openinula/next'; + + function App () { + return <>
    xxx
    + } + render( + App, + document.getElementById('app') + ); + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, createElement as $$createElement, initCompNode as $$initCompNode } from "@openinula/next"; + import { render } from '@openinula/next'; + function App() { + let self; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0; + $node0 = $$createElement("div"); + $node0.textContent = "xxx"; + return [[$node0],,]; + } + }); + return $$initCompNode(self); + } + render(App, document.getElementById('app'));" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/e2e/hook.test.tsx b/packages/transpiler/babel-inula-next-core/test/e2e/hook.test.tsx index 9614b250a0f8ef99923c3b939f51be7c20196079..d04d5945eb4bbfdc9b7399aef624148a46475651 100644 --- a/packages/transpiler/babel-inula-next-core/test/e2e/hook.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/e2e/hook.test.tsx @@ -1,261 +1,261 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { transform } from '../mock'; - -describe('hook', () => { - describe('consumer', () => { - it('should transform use hook', () => { - expect( - transform(` - function App () { - let baseX = 0; - let baseY = 0; - const mouse = useMousePosition(baseX, baseY) - const mouse2 = useMousePosition({ - baseX, - baseY - }) - } - `) - ).toMatchInlineSnapshot(` - "import { untrack as $$untrack, useHook as $$useHook, Comp as $$Comp, createComponent as $$createComponent, runOnce as $$runOnce, notCached as $$notCached } from "@openinula/next"; - function App() { - let self; - let baseX = 0; - let baseY = 0; - let _useMousePosition_$h$_; - let mouse; - let _useMousePosition2_$h$_; - let mouse2; - self = $$createComponent({ - updateState: changed => { - if (changed & 1) { - if ($$notCached(self, Symbol.for("inula-cache"), [baseX])) { - { - $$untrack(() => _useMousePosition_$h$_).updateProp("p0", baseX); - } - } - } - if (changed & 2) { - if ($$notCached(self, Symbol.for("inula-cache"), [baseY])) { - { - $$untrack(() => _useMousePosition_$h$_).updateProp("p1", baseY); - } - } - } - if (changed & 3) { - if ($$notCached(self, Symbol.for("inula-cache"), [baseX, baseY])) { - $$runOnce(() => { - _useMousePosition_$h$_ = $$useHook(useMousePosition, [baseX, baseY], 4); - }); - $$runOnce(() => { - _useMousePosition2_$h$_ = $$useHook(useMousePosition, [{ - baseX, - baseY - }], 8); - }); - { - $$untrack(() => _useMousePosition2_$h$_).updateProp("p0", { - baseX, - baseY - }); - } - } - } - if (changed & 4) { - if ($$notCached(self, Symbol.for("inula-cache"), [_useMousePosition_$h$_?.value])) { - mouse = _useMousePosition_$h$_.value(); - } - } - if (changed & 8) { - if ($$notCached(self, Symbol.for("inula-cache"), [_useMousePosition2_$h$_?.value])) { - mouse2 = _useMousePosition2_$h$_.value(); - } - } - } - }); - return self.init(); - }" - `); - }); - - it('should support multiple return ', () => { - expect( - transform(` - function App2() { - let {count, setCount} = useCounter() - return ( - - ); - }`) - ).toMatchInlineSnapshot(` - "import { useHook as $$useHook, Comp as $$Comp, createComponent as $$createComponent, notCached as $$notCached, createElement as $$createElement, delegateEvent as $$delegateEvent, createTextNode as $$createTextNode, insertNode as $$insertNode, ExpNode as $$ExpNode } from "@openinula/next"; - function App2() { - let self; - let _useCounter_$h$_ = $$useHook(useCounter, [], 1); - let count; - let setCount; - self = $$createComponent({ - updateState: changed => { - if (changed & 1) { - if ($$notCached(self, Symbol.for("inula-cache"), [_useCounter_$h$_?.value])) { - { - self.updateDerived(({ - count, - setCount - } = _useCounter_$h$_.value()), 6 /*0b110*/); - } - } - } - }, - getUpdateViews: () => { - let $node0, $node1, $node2; - $node0 = $$createElement("button"); - $$delegateEvent($node0, "click", setCount); - $node1 = $$createTextNode("Increment ", []); - $$insertNode($node0, $node1, 0); - $node2 = new $$ExpNode(count, [count]); - $$insertNode($node0, $node2, 1); - $node0._$nodes = [$node1, $node2]; - return [[$node0], $changed => { - if ($changed & 2) { - $node2 && $node2.update(() => count, [count]); - } - if ($changed & 4) { - $node0 && $$delegateEvent($node0, "click", setCount); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); - }); - - describe('provider', () => { - it('should transform parameters of hook', () => { - expect( - transform(` - function useMousePosition(baseX, baseY, { settings, id }) { - let x; - let y; - - const {name} = useStore('Store'); - - - function handleMouseMove(event) { - x = baseX + (event.clientX / window.innerWidth) * envX; - y = baseY + (event.clientY / window.innerHeight) * envY; - } - didMount(() => { - window.addEventListener('mousemove', handleMouseMove); - }) - - willUnmount(() => { - window.removeEventListener('mousemove', handleMouseMove); - }) - return [x, y]; - }`) - ).toMatchInlineSnapshot(` - "import { useHook as $$useHook, Comp as $$Comp, createHook as $$createHook, notCached as $$notCached } from "@openinula/next"; - function useMousePosition({ - p0, - p1, - p2 - }) { - let self; - let p0_$p$_ = p0; - let baseX; - let p1_$p$_ = p1; - let baseY; - let p2_$p$_ = p2; - let settings; - let id; - let x; - let y; - let _useStore_$h$_ = $$useHook(useStore, ['Store'], 32); - let name; - function handleMouseMove(event) { - self.updateDerived(x = baseX + event.clientX / window.innerWidth * envX, 8 /*0b1000*/); - self.updateDerived(y = baseY + event.clientY / window.innerHeight * envY, 16 /*0b10000*/); - } - self = $$createHook({ - didMount: () => { - { - window.addEventListener('mousemove', handleMouseMove); - } - }, - willUnmount: () => { - { - window.removeEventListener('mousemove', handleMouseMove); - } - }, - updateState: changed => { - if (changed & 1) { - if ($$notCached(self, Symbol.for("inula-cache"), [p0_$p$_])) { - baseX = p0_$p$_; - } - } - if (changed & 2) { - if ($$notCached(self, Symbol.for("inula-cache"), [p1_$p$_])) { - baseY = p1_$p$_; - } - } - if (changed & 4) { - if ($$notCached(self, Symbol.for("inula-cache"), [p2_$p$_])) { - { - ({ - settings, - id - } = p2_$p$_); - } - } - } - if (changed & 32) { - if ($$notCached(self, Symbol.for("inula-cache"), [_useStore_$h$_?.value])) { - { - ({ - name - } = _useStore_$h$_.value()); - } - } - } - }, - updateProp: (propName, newValue) => { - if (propName === "p2") { - self.updateDerived(p2_$p$_ = newValue, 4 /*0b100*/); - } else if (propName === "p1") { - self.updateDerived(p1_$p$_ = newValue, 2 /*0b10*/); - } else if (propName === "p0") { - self.updateDerived(p0_$p$_ = newValue, 1 /*0b1*/); - } - }, - value: () => [x, y], - getUpdateViews: () => { - return function ($changed) { - if ($changed & 24) self.emitUpdate(); - }; - } - }); - return self.init(); - }" - `); - }); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from '../mock'; + +describe('hook', () => { + describe('consumer', () => { + it('should transform use hook', () => { + expect( + transform(` + function App () { + let baseX = 0; + let baseY = 0; + const mouse = useMousePosition(baseX, baseY) + const mouse2 = useMousePosition({ + baseX, + baseY + }) + } + `) + ).toMatchInlineSnapshot(` + "import { untrack as $$untrack, useHook as $$useHook, Comp as $$Comp, createComponent as $$createComponent, runOnce as $$runOnce, notCached as $$notCached, initCompNode as $$initCompNode } from "@openinula/next"; + function App() { + let self; + let baseX = 0; + let baseY = 0; + let _useMousePosition_$h$_; + let mouse; + let _useMousePosition2_$h$_; + let mouse2; + self = $$createComponent({ + updateState: changed => { + if (changed & 1) { + if ($$notCached(self, "cache0", [baseX])) { + { + $$untrack(() => _useMousePosition_$h$_) && $$untrack(() => _useMousePosition_$h$_).updateProp("p0", baseX); + } + } + } + if (changed & 2) { + if ($$notCached(self, "cache1", [baseY])) { + { + $$untrack(() => _useMousePosition_$h$_) && $$untrack(() => _useMousePosition_$h$_).updateProp("p1", baseY); + } + } + } + if (changed & 3) { + if ($$notCached(self, "cache2", [baseX, baseY])) { + $$runOnce(() => { + _useMousePosition_$h$_ = $$useHook(useMousePosition, [baseX, baseY], 4); + }); + $$runOnce(() => { + _useMousePosition2_$h$_ = $$useHook(useMousePosition, [{ + baseX, + baseY + }], 8); + }); + { + $$untrack(() => _useMousePosition2_$h$_) && $$untrack(() => _useMousePosition2_$h$_).updateProp("p0", { + baseX, + baseY + }); + } + } + } + if (changed & 4) { + if ($$notCached(self, "cache3", [_useMousePosition_$h$_?.value])) { + mouse = _useMousePosition_$h$_.value(); + } + } + if (changed & 8) { + if ($$notCached(self, "cache4", [_useMousePosition2_$h$_?.value])) { + mouse2 = _useMousePosition2_$h$_.value(); + } + } + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should support multiple return ', () => { + expect( + transform(` + function App2() { + let {count, setCount} = useCounter() + return ( + + ); + }`) + ).toMatchInlineSnapshot(` + "import { useHook as $$useHook, Comp as $$Comp, createComponent as $$createComponent, updateNode as $$updateNode, notCached as $$notCached, createElement as $$createElement, delegateEvent as $$delegateEvent, createTextNode as $$createTextNode, appendNode as $$appendNode, createNode as $$createNode, insertNode as $$insertNode, initCompNode as $$initCompNode } from "@openinula/next"; + function App2() { + let self; + let _useCounter_$h$_ = $$useHook(useCounter, [], 1); + let count; + let setCount; + self = $$createComponent({ + updateState: changed => { + if (changed & 1) { + if ($$notCached(self, "cache0", [_useCounter_$h$_?.value])) { + { + $$updateNode(self, ({ + count, + setCount + } = _useCounter_$h$_.value()), 6 /*0b110*/); + } + } + } + }, + getUpdateViews: () => { + let $node0, $node1, $node2; + $node0 = $$createElement("button"); + $$delegateEvent($node0, "click", setCount); + $node1 = $$createTextNode("Increment ", []); + $$appendNode($node0, $node1); + $node2 = $$createNode(3 /*Exp*/, () => count, [count]); + $$insertNode($node0, $node2, 1); + $node0._$nodes = [$node1, $node2]; + return [[$node0], $changed => { + if ($changed & 2) { + $node2 && $$updateNode($node2, () => count, [count]); + } + if ($changed & 4) { + $node0 && $$delegateEvent($node0, "click", setCount); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + }); + + describe('provider', () => { + it('should transform parameters of hook', () => { + expect( + transform(` + function useMousePosition(baseX, baseY, { settings, id }) { + let x; + let y; + + const {name} = useStore('Store'); + + + function handleMouseMove(event) { + x = baseX + (event.clientX / window.innerWidth) * envX; + y = baseY + (event.clientY / window.innerHeight) * envY; + } + didMount(() => { + window.addEventListener('mousemove', handleMouseMove); + }) + + willUnmount(() => { + window.removeEventListener('mousemove', handleMouseMove); + }) + return [x, y]; + }`) + ).toMatchInlineSnapshot(` + "import { useHook as $$useHook, Comp as $$Comp, updateNode as $$updateNode, createHook as $$createHook, notCached as $$notCached, emitUpdate as $$emitUpdate, initCompNode as $$initCompNode } from "@openinula/next"; + function useMousePosition({ + p0, + p1, + p2 + }) { + let self; + let p0_$p$_ = p0; + let baseX; + let p1_$p$_ = p1; + let baseY; + let p2_$p$_ = p2; + let settings; + let id; + let x; + let y; + let _useStore_$h$_ = $$useHook(useStore, ['Store'], 32); + let name; + function handleMouseMove(event) { + $$updateNode(self, x = baseX + event.clientX / window.innerWidth * envX, 8 /*0b1000*/); + $$updateNode(self, y = baseY + event.clientY / window.innerHeight * envY, 16 /*0b10000*/); + } + self = $$createHook({ + didMount: () => { + { + window.addEventListener('mousemove', handleMouseMove); + } + }, + willUnmount: () => { + { + window.removeEventListener('mousemove', handleMouseMove); + } + }, + updateState: changed => { + if (changed & 1) { + if ($$notCached(self, "cache0", [p0_$p$_])) { + baseX = p0_$p$_; + } + } + if (changed & 2) { + if ($$notCached(self, "cache1", [p1_$p$_])) { + baseY = p1_$p$_; + } + } + if (changed & 4) { + if ($$notCached(self, "cache2", [p2_$p$_])) { + { + ({ + settings, + id + } = p2_$p$_); + } + } + } + if (changed & 32) { + if ($$notCached(self, "cache3", [_useStore_$h$_?.value])) { + { + ({ + name + } = _useStore_$h$_.value()); + } + } + } + }, + updateProp: (propName, newValue) => { + if (propName === "p2") { + $$updateNode(self, p2_$p$_ = newValue, 4 /*0b100*/); + } else if (propName === "p1") { + $$updateNode(self, p1_$p$_ = newValue, 2 /*0b10*/); + } else if (propName === "p0") { + $$updateNode(self, p0_$p$_ = newValue, 1 /*0b1*/); + } + }, + value: () => [x, y], + getUpdateViews: () => { + return function ($changed) { + if ($changed & 24) $$emitUpdate(self); + }; + } + }); + return $$initCompNode(self); + }" + `); + }); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/e2e/mappingToFor.test.ts b/packages/transpiler/babel-inula-next-core/test/e2e/mappingToFor.test.ts index e2f1c618d258e17a8761830e46fc07f0268055cc..031764c4c1de0526a13eade7f39cdd4cd6ec432a 100644 --- a/packages/transpiler/babel-inula-next-core/test/e2e/mappingToFor.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/e2e/mappingToFor.test.ts @@ -1,82 +1,119 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { transform } from '../mock'; - -describe('mapping2ForPlugin', () => { - it('should transform map to for jsxelement', () => { - const code = ` - function MyComp() { - let arr = [1, 2, 3]; - return ( - <> - {arr.map((item) => (
    {item}
    ))} - - ) - } - `; - const transformedCode = transform(code); - expect(transformedCode).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, createElement as $$createElement, ExpNode as $$ExpNode, insertNode as $$insertNode, ForNode as $$ForNode } from "@openinula/next"; - function MyComp() { - let self; - let arr = [1, 2, 3]; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0; - $node0 = new $$ForNode(arr, 1, null, (item, $idx, $updateArr) => { - let $node0, $node1; - $updateArr[$idx] = ($changed, $item) => { - item = $item; - if ($changed & 1) { - $node1 && $node1.update(() => item, [item]); - } - }; - $node0 = $$createElement("div"); - $node1 = new $$ExpNode(item, [item]); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [$node0]; - }); - return [[$node0], $changed => { - if ($changed & 1) { - $node0 && $node0.updateArray(arr, null); - } - $node0 && $node0.update($changed); - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); - it.fails('should transform last map to for ', () => { - const code = ` - function MyComp() { - let arr = [1, 2, 3]; - return ( -
    - {arr.map(item =>

    {item}

    ).map((item) => (
    {item}
    ))} -
    - ) - } - `; - const transformedCode = transform(code); - expect(transformedCode).toMatchInlineSnapshot('TODO'); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from '../mock'; + +describe('mapping2ForPlugin', () => { + it('should transform map to for jsxelement', () => { + const code = ` + function MyComp() { + let arr = [1, 2, 3]; + return ( + <> + {arr.map((item) => (
    {item}
    ))} + + ) + } + `; + const transformedCode = transform(code); + expect(transformedCode).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, createElement as $$createElement, createNode as $$createNode, updateNode as $$updateNode, insertNode as $$insertNode, updateChildren as $$updateChildren, initCompNode as $$initCompNode } from "@openinula/next"; + function MyComp() { + let self; + let arr = [1, 2, 3]; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0; + $node0 = $$createNode(1 /*For*/, arr, 1, null, (item, $idx, $updateArr) => { + let $node0, $node1; + $updateArr[$idx] = ($changed, $item) => { + item = $item; + if ($changed & 1) { + $node1 && $$updateNode($node1, () => item, [item]); + } + }; + $node0 = $$createElement("div"); + $node1 = $$createNode(3 /*Exp*/, () => item, [item]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [$node0]; + }); + return [[$node0], $changed => { + if ($changed & 1) { + $node0 && $$updateNode($node0, arr, null); + } + $node0 && $$updateChildren($node0, $changed); + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + it.fails('should transform last map to for ', () => { + const code = ` + function MyComp() { + let arr = [1, 2, 3]; + return ( +
    + {arr.map(item =>

    {item}

    ).map((item) => (
    {item}
    ))} +
    + ) + } + `; + const transformedCode = transform(code); + expect(transformedCode).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, createElement as $$createElement, createNode as $$createNode, updateNode as $$updateNode, insertNode as $$insertNode, updateChildren as $$updateChildren, initCompNode as $$initCompNode } from "@openinula/next"; + function MyComp() { + let self; + let arr = [1, 2, 3]; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1; + $node0 = $$createElement("div"); + $node1 = $$createNode(1 /*For*/, arr.map(item =>

    {item}

    ), 1, null, (item, $idx, $updateArr) => { + let $node0, $node1; + $updateArr[$idx] = ($changed, $item) => { + item = $item; + if ($changed & 1) { + $node1 && $$updateNode($node1, () => item, [item]); + } + }; + $node0 = $$createElement("div"); + $node1 = $$createNode(3 /*Exp*/, () => item, [item]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [$node0]; + }); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [[$node0], $changed => { + if ($changed & 1) { + $node1 && $$updateNode($node1, arr.map(item =>

    {item}

    ), null); + } + $node1 && $$updateChildren($node1, $changed); + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/e2e/props.test.tsx b/packages/transpiler/babel-inula-next-core/test/e2e/props.test.tsx index 5ee9a7099d755001b63dddc1f80bfc2985de85d7..74943cfc83d8fd8f018be4288a11b8c8e4ba5df6 100644 --- a/packages/transpiler/babel-inula-next-core/test/e2e/props.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/e2e/props.test.tsx @@ -1,206 +1,206 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { transform } from '../mock'; - -describe('props', () => { - it('should pass empty object without props', () => { - expect( - transform(` - function App() { - return ; - } - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, Comp as $$Comp } from "@openinula/next"; - function App() { - let self; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0; - $node0 = $$Comp(Child, {}); - return [[$node0],,]; - } - }); - return self.init(); - }" - `); - }); - - it('should handle props destructing', () => { - expect( - transform(` - function App({ id, className = 'default' }) { - return
    -
    ; - } - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, createElement as $$createElement, setHTMLProp as $$setHTMLProp } from "@openinula/next"; - function App({ - id, - className = 'default' - }) { - let self; - let id_$p$_ = id; - let className_$p$_ = className; - self = $$createComponent({ - updateState: changed => {}, - updateProp: (propName, newValue) => { - if (propName === "className") { - self.updateDerived(className_$p$_ = newValue, 2 /*0b10*/); - } else if (propName === "id") { - self.updateDerived(id_$p$_ = newValue, 1 /*0b1*/); - } - }, - getUpdateViews: () => { - let $node0; - $node0 = $$createElement("div"); - $$setHTMLProp($node0, "id", () => id_$p$_, [id_$p$_]); - $$setHTMLProp($node0, "className", () => className_$p$_, [className_$p$_]); - return [[$node0], $changed => { - if ($changed & 1) { - $node0 && $$setHTMLProp($node0, "id", () => id_$p$_, [id_$p$_]); - } - if ($changed & 2) { - $node0 && $$setHTMLProp($node0, "className", () => className_$p$_, [className_$p$_]); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); - - it('should handle props nested destructing', () => { - expect( - transform(` - function App({ info: { id, className = 'default', pos: [x, y] } }) { - return
    -
    ; - } - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, notCached as $$notCached, createElement as $$createElement, setHTMLProp as $$setHTMLProp, setStyle as $$setStyle } from "@openinula/next"; - function App({ - info - }) { - let self; - let info_$p$_ = info; - let id; - let className; - let x; - let y; - self = $$createComponent({ - updateState: changed => { - if (changed & 1) { - if ($$notCached(self, Symbol.for("inula-cache"), [info_$p$_])) { - { - self.updateDerived(({ - id, - className = 'default', - pos: [x, y] - } = info_$p$_), 30 /*0b11110*/); - } - } - } - }, - updateProp: (propName, newValue) => { - if (propName === "info") { - self.updateDerived(info_$p$_ = newValue, 1 /*0b1*/); - } - }, - getUpdateViews: () => { - let $node0; - $node0 = $$createElement("div"); - $$setHTMLProp($node0, "id", () => id, [id]); - $$setHTMLProp($node0, "className", () => className, [className]); - $$setStyle($node0, { - left: x, - top: y - }); - return [[$node0], $changed => { - if ($changed & 2) { - $node0 && $$setHTMLProp($node0, "id", () => id, [id]); - } - if ($changed & 4) { - $node0 && $$setHTMLProp($node0, "className", () => className, [className]); - } - if ($changed & 24) { - $node0 && $$setStyle($node0, { - left: x, - top: y - }); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); - - it('should support alias', () => { - expect( - transform(/**jsx*/ ` - function Child({ name: alias }) { - return

    {alias}

    ; - } - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, notCached as $$notCached, createElement as $$createElement, ExpNode as $$ExpNode, insertNode as $$insertNode } from "@openinula/next"; - function Child({ - name - }) { - let self; - let name_$p$_ = name; - let alias; - self = $$createComponent({ - updateState: changed => { - if (changed & 1) { - if ($$notCached(self, Symbol.for("inula-cache"), [name_$p$_])) { - self.updateDerived(alias = name_$p$_, 2 /*0b10*/); - } - } - }, - updateProp: (propName, newValue) => { - if (propName === "name") { - self.updateDerived(name_$p$_ = newValue, 1 /*0b1*/); - } - }, - getUpdateViews: () => { - let $node0, $node1; - $node0 = $$createElement("h1"); - $node1 = new $$ExpNode(alias, [alias]); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [[$node0], $changed => { - if ($changed & 2) { - $node1 && $node1.update(() => alias, [alias]); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from '../mock'; + +describe('props', () => { + it('should pass empty object without props', () => { + expect( + transform(` + function App() { + return ; + } + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, Comp as $$Comp, initCompNode as $$initCompNode } from "@openinula/next"; + function App() { + let self; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0; + $node0 = $$Comp(Child, {}); + return [[$node0],,]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should handle props destructing', () => { + expect( + transform(` + function App({ id, className = 'default' }) { + return
    +
    ; + } + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, updateNode as $$updateNode, createElement as $$createElement, setHTMLProp as $$setHTMLProp, initCompNode as $$initCompNode } from "@openinula/next"; + function App({ + id, + className = 'default' + }) { + let self; + let id_$p$_ = id; + let className_$p$_ = className; + self = $$createComponent({ + updateState: changed => {}, + updateProp: (propName, newValue) => { + if (propName === "className") { + $$updateNode(self, className_$p$_ = newValue, 2 /*0b10*/); + } else if (propName === "id") { + $$updateNode(self, id_$p$_ = newValue, 1 /*0b1*/); + } + }, + getUpdateViews: () => { + let $node0; + $node0 = $$createElement("div"); + $$setHTMLProp($node0, "id", () => id_$p$_, [id_$p$_]); + $$setHTMLProp($node0, "className", () => className_$p$_, [className_$p$_]); + return [[$node0], $changed => { + if ($changed & 1) { + $node0 && $$setHTMLProp($node0, "id", () => id_$p$_, [id_$p$_]); + } + if ($changed & 2) { + $node0 && $$setHTMLProp($node0, "className", () => className_$p$_, [className_$p$_]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should handle props nested destructing', () => { + expect( + transform(` + function App({ info: { id, className = 'default', pos: [x, y] } }) { + return
    +
    ; + } + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, updateNode as $$updateNode, notCached as $$notCached, createElement as $$createElement, setHTMLProp as $$setHTMLProp, setStyle as $$setStyle, initCompNode as $$initCompNode } from "@openinula/next"; + function App({ + info + }) { + let self; + let info_$p$_ = info; + let id; + let className; + let x; + let y; + self = $$createComponent({ + updateState: changed => { + if (changed & 1) { + if ($$notCached(self, "cache0", [info_$p$_])) { + { + $$updateNode(self, ({ + id, + className = 'default', + pos: [x, y] + } = info_$p$_), 30 /*0b11110*/); + } + } + } + }, + updateProp: (propName, newValue) => { + if (propName === "info") { + $$updateNode(self, info_$p$_ = newValue, 1 /*0b1*/); + } + }, + getUpdateViews: () => { + let $node0; + $node0 = $$createElement("div"); + $$setHTMLProp($node0, "id", () => id, [id]); + $$setHTMLProp($node0, "className", () => className, [className]); + $$setStyle($node0, { + left: x, + top: y + }); + return [[$node0], $changed => { + if ($changed & 2) { + $node0 && $$setHTMLProp($node0, "id", () => id, [id]); + } + if ($changed & 4) { + $node0 && $$setHTMLProp($node0, "className", () => className, [className]); + } + if ($changed & 24) { + $node0 && $$setStyle($node0, { + left: x, + top: y + }); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should support alias', () => { + expect( + transform(/**jsx*/ ` + function Child({ name: alias }) { + return

    {alias}

    ; + } + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, updateNode as $$updateNode, notCached as $$notCached, createElement as $$createElement, createNode as $$createNode, insertNode as $$insertNode, initCompNode as $$initCompNode } from "@openinula/next"; + function Child({ + name + }) { + let self; + let name_$p$_ = name; + let alias; + self = $$createComponent({ + updateState: changed => { + if (changed & 1) { + if ($$notCached(self, "cache0", [name_$p$_])) { + $$updateNode(self, alias = name_$p$_, 2 /*0b10*/); + } + } + }, + updateProp: (propName, newValue) => { + if (propName === "name") { + $$updateNode(self, name_$p$_ = newValue, 1 /*0b1*/); + } + }, + getUpdateViews: () => { + let $node0, $node1; + $node0 = $$createElement("h1"); + $node1 = $$createNode(3 /*Exp*/, () => alias, [alias]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [[$node0], $changed => { + if ($changed & 2) { + $node1 && $$updateNode($node1, () => alias, [alias]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/e2e/ref.test.tsx b/packages/transpiler/babel-inula-next-core/test/e2e/ref.test.tsx index 789a8c0c62215d17045082a90e8214b334414c71..1eeca1a2a2bee13b44444d42d5b82b13ff56a203 100644 --- a/packages/transpiler/babel-inula-next-core/test/e2e/ref.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/e2e/ref.test.tsx @@ -1,86 +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 { transform } from '../mock'; - -describe('ref', () => { - it('should transform ref forwarding', () => { - expect( - transform(` - function App() { - let count = 0; - let ref; - return test; - } - - function Input({ ref }) { - return ; - } - - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, createTextNode as $$createTextNode, PropView as $$PropView, Comp as $$Comp, createElement as $$createElement } from "@openinula/next"; - function App() { - let self; - let count = 0; - let ref; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1; - $node1 = new $$PropView($addUpdate => { - let $node0; - $node0 = $$createTextNode("test", []); - return [$node0]; - }); - $node0 = $$Comp(Input, { - "ref": function ($el) { - typeof ref === "function" ? ref($el) : self.updateDerived(ref = $el, 1 /*0b1*/); - }, - "children": $node1 - }); - return [[$node0], $changed => { - $node1 && $node1.update($changed); - return [$node0]; - }]; - } - }); - return self.init(); - } - function Input({ - ref - }) { - let self; - let ref_$p$_ = ref; - self = $$createComponent({ - updateState: changed => {}, - updateProp: (propName, newValue) => { - if (propName === "ref") { - self.updateDerived(ref_$p$_ = newValue, 1 /*0b1*/); - } - }, - getUpdateViews: () => { - let $node0; - $node0 = $$createElement("input"); - typeof ref_$p$_ === "function" ? ref_$p$_($node0) : ref_$p$_ = $node0; - return [[$node0],,]; - } - }); - return self.init(); - }" - `); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from '../mock'; + +describe('ref', () => { + it('should transform ref forwarding', () => { + expect( + transform(` + function App() { + let count = 0; + let ref; + return test; + } + + function Input({ ref }) { + return ; + } + + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, createTextNode as $$createTextNode, createNode as $$createNode, updateNode as $$updateNode, Comp as $$Comp, initCompNode as $$initCompNode, createElement as $$createElement } from "@openinula/next"; + function App() { + let self; + let count = 0; + let ref; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1; + $node1 = $$createNode(6 /*Children*/, $addUpdate => { + let $node0; + $node0 = $$createTextNode("test", []); + return [$node0]; + }); + $node0 = $$Comp(Input, { + "ref": function ($el) { + typeof ref === "function" ? ref($el) : $$updateNode(self, ref = $el, 1 /*0b1*/); + }, + "children": $node1 + }); + return [[$node0], $changed => { + $$updateNode($node1, $changed); + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + } + function Input({ + ref + }) { + let self; + let ref_$p$_ = ref; + self = $$createComponent({ + updateState: changed => {}, + updateProp: (propName, newValue) => { + if (propName === "ref") { + $$updateNode(self, ref_$p$_ = newValue, 1 /*0b1*/); + } + }, + getUpdateViews: () => { + let $node0; + $node0 = $$createElement("input"); + typeof ref_$p$_ === "function" ? ref_$p$_($node0) : ref_$p$_ = $node0; + return [[$node0],,]; + } + }); + return $$initCompNode(self); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/e2e/state.test.tsx b/packages/transpiler/babel-inula-next-core/test/e2e/state.test.tsx index 24d6199e8ccfd6033069b3b31c1b3aaabd11451b..e1e6ff40a9322938e96d3c61f2ac0aa7fed7e645 100644 --- a/packages/transpiler/babel-inula-next-core/test/e2e/state.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/e2e/state.test.tsx @@ -1,67 +1,122 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { transform } from '../mock'; - -describe('state', () => { - it('should transform destructing state', () => { - expect( - transform(` - function App() { - const [count, setCount] = useState(0); - return
    - {count} is smaller than 1 -
    ; - } - `) - ).toMatchInlineSnapshot(` - "import { useHook as $$useHook, Comp as $$Comp, createComponent as $$createComponent, notCached as $$notCached, createElement as $$createElement, ExpNode as $$ExpNode, insertNode as $$insertNode, createTextNode as $$createTextNode } from "@openinula/next"; - function App() { - let self; - let _useState_$h$_ = $$useHook(useState, [0], 1); - let count; - let setCount; - self = $$createComponent({ - updateState: changed => { - if (changed & 1) { - if ($$notCached(self, Symbol.for("inula-cache"), [_useState_$h$_?.value])) { - { - self.updateDerived([count, setCount] = _useState_$h$_.value(), 2 /*0b10*/); - } - } - } - }, - getUpdateViews: () => { - let $node0, $node1, $node2; - $node0 = $$createElement("div"); - $node1 = new $$ExpNode(count, [count]); - $$insertNode($node0, $node1, 0); - $node2 = $$createTextNode(" is smaller than 1", []); - $$insertNode($node0, $node2, 1); - $node0._$nodes = [$node1, $node2]; - return [[$node0], $changed => { - if ($changed & 2) { - $node1 && $node1.update(() => count, [count]); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from '../mock'; + +describe('state', () => { + it('should transform destructing state', () => { + expect( + transform(` + function App() { + let x = 0; + const [count, setCount] = genState(x); + return
    + {count} is smaller than 1 +
    ; + } + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, updateNode as $$updateNode, notCached as $$notCached, createElement as $$createElement, createNode as $$createNode, insertNode as $$insertNode, createTextNode as $$createTextNode, appendNode as $$appendNode, initCompNode as $$initCompNode } from "@openinula/next"; + function App() { + let self; + let x = 0; + let count; + let setCount; + self = $$createComponent({ + updateState: changed => { + if (changed & 1) { + if ($$notCached(self, "cache0", [x])) { + { + $$updateNode(self, [count, setCount] = genState(x), 2 /*0b10*/); + } + } + } + }, + getUpdateViews: () => { + let $node0, $node1, $node2; + $node0 = $$createElement("div"); + $node1 = $$createNode(3 /*Exp*/, () => count, [count]); + $$insertNode($node0, $node1, 0); + $node2 = $$createTextNode(" is smaller than 1", []); + $$appendNode($node0, $node2); + $node0._$nodes = [$node1, $node2]; + return [[$node0], $changed => { + if ($changed & 2) { + $node1 && $$updateNode($node1, () => count, [count]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should transform functional dependency state', () => { + expect( + transform(` + function App() { + let x = 1; + const double = x * 2; + const quadruple = double * 2; + const getQuadruple = () => quadruple; + const y = getQuadruple() + x; + return
    {y}
    ; + } + `) + ).toMatchInlineSnapshot(` + "import { Comp as $$Comp, createComponent as $$createComponent, updateNode as $$updateNode, notCached as $$notCached, createElement as $$createElement, createNode as $$createNode, insertNode as $$insertNode, initCompNode as $$initCompNode } from "@openinula/next"; + function App() { + let self; + let x = 1; + let double; + let quadruple; + const getQuadruple = () => quadruple; + let y; + self = $$createComponent({ + updateState: changed => { + if (changed & 1) { + if ($$notCached(self, "cache0", [x])) { + $$updateNode(self, double = x * 2, 2 /*0b10*/); + $$updateNode(self, y = getQuadruple() + x, 4 /*0b100*/); + } + } + if (changed & 2) { + if ($$notCached(self, "cache1", [double])) { + quadruple = double * 2; + } + } + }, + getUpdateViews: () => { + let $node0, $node1; + $node0 = $$createElement("div"); + $node1 = $$createNode(3 /*Exp*/, () => y, [y]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [[$node0], $changed => { + if ($changed & 4) { + $node1 && $$updateNode($node1, () => y, [y]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/e2e/subComponent.test.tsx b/packages/transpiler/babel-inula-next-core/test/e2e/subComponent.test.tsx index 63123ba77b309e918586210b4cecf1e6c45f006f..4dd98479822e539c634a64df797024a2f228d0dc 100644 --- a/packages/transpiler/babel-inula-next-core/test/e2e/subComponent.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/e2e/subComponent.test.tsx @@ -1,136 +1,136 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { transform } from '../mock'; - -describe('nested component', () => { - it('should transform subComponent', () => { - expect( - transform(` - function App() { - let val = 123; - function Input() { - let double = val * 2 - return ; - } - return
    ; - } - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, notCached as $$notCached, createElement as $$createElement, setHTMLProp as $$setHTMLProp, Comp as $$Comp, insertNode as $$insertNode } from "@openinula/next"; - function App() { - let self; - let val = 123; - function Input() { - let self1; - let double; - self1 = $$createComponent({ - updateState: changed => { - if (changed & 1) { - if ($$notCached(self1, Symbol.for("inula-cache"), [val])) { - self1.updateDerived(double = val * 2, 2 /*0b10*/); - } - } - }, - getUpdateViews: () => { - let $node0; - $node0 = $$createElement("input"); - $$setHTMLProp($node0, "value", () => double, [double]); - return [[$node0], $changed => { - if ($changed & 2) { - $node0 && $$setHTMLProp($node0, "value", () => double, [double]); - } - return [$node0]; - }]; - } - }); - return self1.init(); - } - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1; - $node0 = $$createElement("div"); - $node1 = $$Comp(Input, {}); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [[$node0], $changed => { - if ($changed & 3) { - $node1.updateDerived(null, 3); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); - - it('should transform JSX slice', () => { - expect( - transform(` - function App() { - let val = 123; - const input = ; - return
    {input}
    ; - } - `) - ).toMatchInlineSnapshot(` - "import { Comp as $$Comp, createComponent as $$createComponent, createElement as $$createElement, setHTMLProp as $$setHTMLProp, ExpNode as $$ExpNode, insertNode as $$insertNode } from "@openinula/next"; - function App() { - let self; - let val = 123; - function JSX_input() { - let self1; - self1 = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0; - $node0 = $$createElement("input"); - $$setHTMLProp($node0, "value", () => val, [val]); - return [[$node0], $changed => { - if ($changed & 1) { - $node0 && $$setHTMLProp($node0, "value", () => val, [val]); - } - return [$node0]; - }]; - } - }); - return self1.init(); - } - const input = $$Comp(JSX_input); - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1; - $node0 = $$createElement("div"); - $node1 = new $$ExpNode(input, [input]); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [[$node0], $changed => { - if ($changed & 2) { - $node1 && $node1.update(() => input, [input]); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from '../mock'; + +describe('nested component', () => { + it('should transform subComponent', () => { + expect( + transform(` + function App() { + let val = 123; + function Input() { + let double = val * 2 + return ; + } + return
    ; + } + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, updateNode as $$updateNode, notCached as $$notCached, createElement as $$createElement, setHTMLProp as $$setHTMLProp, initCompNode as $$initCompNode, Comp as $$Comp, insertNode as $$insertNode } from "@openinula/next"; + function App() { + let self; + let val = 123; + function Input() { + let self1; + let double; + self1 = $$createComponent({ + updateState: changed => { + if (changed & 1) { + if ($$notCached(self1, "cache0", [val])) { + $$updateNode(self1, double = val * 2, 2 /*0b10*/); + } + } + }, + getUpdateViews: () => { + let $node0; + $node0 = $$createElement("input"); + $$setHTMLProp($node0, "value", () => double, [double]); + return [[$node0], $changed => { + if ($changed & 2) { + $node0 && $$setHTMLProp($node0, "value", () => double, [double]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self1); + } + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1; + $node0 = $$createElement("div"); + $node1 = $$Comp(Input, {}); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [[$node0], $changed => { + if ($changed & 3) { + $$updateNode($node1, null, 3); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should transform JSX slice', () => { + expect( + transform(` + function App() { + let val = 123; + const input = ; + return
    {input}
    ; + } + `) + ).toMatchInlineSnapshot(` + "import { Comp as $$Comp, createComponent as $$createComponent, createElement as $$createElement, setHTMLProp as $$setHTMLProp, initCompNode as $$initCompNode, createNode as $$createNode, updateNode as $$updateNode, insertNode as $$insertNode } from "@openinula/next"; + function App() { + let self; + let val = 123; + function JSX_input() { + let self1; + self1 = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0; + $node0 = $$createElement("input"); + $$setHTMLProp($node0, "value", () => val, [val]); + return [[$node0], $changed => { + if ($changed & 1) { + $node0 && $$setHTMLProp($node0, "value", () => val, [val]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self1); + } + const input = $$Comp(JSX_input); + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1; + $node0 = $$createElement("div"); + $node1 = $$createNode(3 /*Exp*/, () => input, [input]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [[$node0], $changed => { + if ($changed & 2) { + $node1 && $$updateNode($node1, () => input, [input]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/e2e/template.test.tsx b/packages/transpiler/babel-inula-next-core/test/e2e/template.test.tsx index ea9542f838fb2bac7574379aef3ba3a063a54ad9..9cfdfacc21b259d8a18881c3fa492833837d1a9f 100644 --- a/packages/transpiler/babel-inula-next-core/test/e2e/template.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/e2e/template.test.tsx @@ -1,256 +1,247 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { transform } from '../mock'; - -describe('template', () => { - it('should transform multi layer html into template', () => { - expect( - transform(` - function App() { - return
    test
    ; - } - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, createElement as $$createElement } from "@openinula/next"; - const _$t = (() => { - let $node0, $node1; - $node0 = $$createElement("div"); - $node1 = $$createElement("div"); - $node1.textContent = "test"; - $node0.appendChild($node1); - return $node0; - })(); - function App() { - let self; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0; - $node0 = _$t.cloneNode(true); - return [[$node0],,]; - } - }); - return self.init(); - }" - `); - }); - - it('should support multi components with template', () => { - expect( - transform(` - function App() { - return
    test
    ; - } - function Title() { - return

    Title

    ; - } - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, createElement as $$createElement } from "@openinula/next"; - const _$t2 = (() => { - let $node0, $node1; - $node0 = $$createElement("div"); - $node1 = $$createElement("h1"); - $node1.textContent = "Title"; - $node0.appendChild($node1); - return $node0; - })(); - const _$t = (() => { - let $node0, $node1; - $node0 = $$createElement("div"); - $node1 = $$createElement("div"); - $node1.textContent = "test"; - $node0.appendChild($node1); - return $node0; - })(); - function App() { - let self; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0; - $node0 = _$t.cloneNode(true); - return [[$node0],,]; - } - }); - return self.init(); - } - function Title() { - let self; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0; - $node0 = _$t2.cloneNode(true); - return [[$node0],,]; - } - }); - return self.init(); - }" - `); - }); - it('should support nested components', () => { - expect( - transform(` - function App() { - let name = 'Alice'; - - function Parent({ children }) { - return ( -
    -

    Parent

    - {children} -
    - ); - } - - function Child({ name }: { name: string }) { - return
    Hello, {name}!
    ; - } - - return ( - - - - ); - } - `) - ).toMatchInlineSnapshot(` - "import { createComponent as $$createComponent, createElement as $$createElement, ExpNode as $$ExpNode, insertNode as $$insertNode, notCached as $$notCached, createTextNode as $$createTextNode, Comp as $$Comp, PropView as $$PropView } from "@openinula/next"; - const _$t2 = (() => { - let $node0, $node1; - $node0 = $$createElement("div"); - $node0.className = "parent"; - $node1 = $$createElement("h2"); - $node1.textContent = "Parent"; - $node0.appendChild($node1); - return $node0; - })(); - const _$t = (() => { - let $node0, $node1; - $node0 = $$createElement("div"); - $node0.className = "parent"; - $node1 = $$createElement("h2"); - $node1.textContent = "Parent"; - $node0.appendChild($node1); - return $node0; - })(); - function App() { - let self; - let name = 'Alice'; - function Parent({ - children - }) { - let self1; - let children_$p$_ = children; - self1 = $$createComponent({ - updateState: changed => {}, - updateProp: (propName, newValue) => { - if (propName === "children") { - self1.updateDerived(children_$p$_ = newValue, 2 /*0b10*/); - } - }, - getUpdateViews: () => { - let $node0, $node1; - $node0 = _$t.cloneNode(true); - $node1 = new $$ExpNode(children_$p$_, [children_$p$_]); - $$insertNode($node0, $node1, 1); - return [[$node0], $changed => { - if ($changed & 2) { - $node1 && $node1.update(() => children_$p$_, [children_$p$_]); - } - return [$node0]; - }]; - } - }); - return self1.init(); - } - function Child({ - name - }) { - let self1; - let name_$p$_; - self1 = $$createComponent({ - updateState: changed => { - if (changed & 1) { - if ($$notCached(self1, Symbol.for("inula-cache"), [name])) { - self1.updateDerived(name_$p$_ = name, 2 /*0b10*/); - } - } - }, - updateProp: (propName, newValue) => { - if (propName === "name") { - self1.updateDerived(name_$p$_ = newValue, 2 /*0b10*/); - } - }, - getUpdateViews: () => { - let $node0, $node1, $node2, $node3; - $node0 = $$createElement("div"); - $node0.className = "child"; - $node1 = $$createTextNode("Hello, ", []); - $$insertNode($node0, $node1, 0); - $node2 = new $$ExpNode(name_$p$_, [name_$p$_]); - $$insertNode($node0, $node2, 1); - $node3 = $$createTextNode("!", []); - $$insertNode($node0, $node3, 2); - $node0._$nodes = [$node1, $node2, $node3]; - return [[$node0], $changed => { - if ($changed & 2) { - $node2 && $node2.update(() => name_$p$_, [name_$p$_]); - } - return [$node0]; - }]; - } - }); - return self1.init(); - } - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1; - $node1 = new $$PropView($addUpdate => { - let $node0; - $addUpdate($changed => { - if ($changed & 1) { - $node0 && $node0._$setProp("name", () => name, [name]); - } - if ($changed & 3) { - $node0.updateDerived(null, 3); - } - }); - $node0 = $$Comp(Child, { - "name": name - }); - return [$node0]; - }); - $node0 = $$Comp(Parent, { - "children": $node1 - }); - return [[$node0], $changed => { - if ($changed & 2) { - $node0.updateDerived(null, 2); - } - $node1 && $node1.update($changed); - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from '../mock'; + +describe('template', () => { + it('should transform multi layer html into template', () => { + expect( + transform(` + function App() { + return
    test
    ; + } + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, createElement as $$createElement, appendNode as $$appendNode, initCompNode as $$initCompNode } from "@openinula/next"; + const _$t = (() => { + let $node0, $node1; + $node0 = $$createElement("div"); + $node1 = $$createElement("div"); + $node1.textContent = "test"; + $$appendNode($node0, $node1); + return $node0; + })(); + function App() { + let self; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0; + $node0 = _$t.cloneNode(true); + return [[$node0],,]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should support multi components with template', () => { + expect( + transform(` + function App() { + return
    test
    ; + } + function Title() { + return

    Title

    ; + } + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, createElement as $$createElement, appendNode as $$appendNode, initCompNode as $$initCompNode } from "@openinula/next"; + const _$t2 = (() => { + let $node0, $node1; + $node0 = $$createElement("div"); + $node1 = $$createElement("h1"); + $node1.textContent = "Title"; + $$appendNode($node0, $node1); + return $node0; + })(); + const _$t = (() => { + let $node0, $node1; + $node0 = $$createElement("div"); + $node1 = $$createElement("div"); + $node1.textContent = "test"; + $$appendNode($node0, $node1); + return $node0; + })(); + function App() { + let self; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0; + $node0 = _$t.cloneNode(true); + return [[$node0],,]; + } + }); + return $$initCompNode(self); + } + function Title() { + let self; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0; + $node0 = _$t2.cloneNode(true); + return [[$node0],,]; + } + }); + return $$initCompNode(self); + }" + `); + }); + it('should support nested components', () => { + expect( + transform(` + function App() { + let name = 'Alice'; + + function Parent({ children }) { + return ( +
    +

    Parent

    + {children} +
    + ); + } + + function Child({ name }: { name: string }) { + return
    Hello, {name}!
    ; + } + + return ( + + + + ); + } + `) + ).toMatchInlineSnapshot(` + "import { createComponent as $$createComponent, updateNode as $$updateNode, createElement as $$createElement, appendNode as $$appendNode, createNode as $$createNode, insertNode as $$insertNode, initCompNode as $$initCompNode, notCached as $$notCached, createTextNode as $$createTextNode, Comp as $$Comp, setProp as $$setProp } from "@openinula/next"; + const _$t = (() => { + let $node0, $node1; + $node0 = $$createElement("div"); + $node0.className = "parent"; + $node1 = $$createElement("h2"); + $node1.textContent = "Parent"; + $$appendNode($node0, $node1); + return $node0; + })(); + function App() { + let self; + let name = 'Alice'; + function Parent({ + children + }) { + let self1; + let children_$p$_ = children; + self1 = $$createComponent({ + updateState: changed => {}, + updateProp: (propName, newValue) => { + if (propName === "children") { + $$updateNode(self1, children_$p$_ = newValue, 2 /*0b10*/); + } + }, + getUpdateViews: () => { + let $node0, $node1; + $node0 = _$t.cloneNode(true); + $node1 = $$createNode(3 /*Exp*/, () => children_$p$_, [children_$p$_]); + $$insertNode($node0, $node1, 1); + return [[$node0], $changed => { + if ($changed & 2) { + $node1 && $$updateNode($node1, () => children_$p$_, [children_$p$_]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self1); + } + function Child({ + name + }) { + let self1; + let name_$p$_; + self1 = $$createComponent({ + updateState: changed => { + if (changed & 1) { + if ($$notCached(self1, "cache0", [name])) { + $$updateNode(self1, name_$p$_ = name, 2 /*0b10*/); + } + } + }, + updateProp: (propName, newValue) => { + if (propName === "name") { + $$updateNode(self1, name_$p$_ = newValue, 2 /*0b10*/); + } + }, + getUpdateViews: () => { + let $node0, $node1, $node2, $node3; + $node0 = $$createElement("div"); + $node0.className = "child"; + $node1 = $$createTextNode("Hello, ", []); + $$appendNode($node0, $node1); + $node2 = $$createNode(3 /*Exp*/, () => name_$p$_, [name_$p$_]); + $$insertNode($node0, $node2, 1); + $node3 = $$createTextNode("!", []); + $$appendNode($node0, $node3); + $node0._$nodes = [$node1, $node2, $node3]; + return [[$node0], $changed => { + if ($changed & 2) { + $node2 && $$updateNode($node2, () => name_$p$_, [name_$p$_]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self1); + } + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1; + $node1 = $$createNode(6 /*Children*/, $addUpdate => { + let $node0; + $addUpdate($changed => { + if ($changed & 1) { + $node0 && $$setProp($node0, "name", () => name, [name]); + } + if ($changed & 3) { + $$updateNode($node0, null, 3); + } + }); + $node0 = $$Comp(Child, { + "name": name + }); + return [$node0]; + }); + $node0 = $$Comp(Parent, { + "children": $node1 + }); + return [[$node0], $changed => { + if ($changed & 2) { + $$updateNode($node0, null, 2); + } + $$updateNode($node1, $changed); + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/generator/comp.test.tsx b/packages/transpiler/babel-inula-next-core/test/generator/comp.test.tsx index 38d2ad3f12989e1b23d220dfbacf7b7880356b51..886b826eccb001b3b93c19376b60ae6685066253 100644 --- a/packages/transpiler/babel-inula-next-core/test/generator/comp.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/generator/comp.test.tsx @@ -1,62 +1,62 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { transform } from './mock'; - -describe('generate', () => { - it('should generate createComponent', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let count = 1; - - return -
    1
    -
    ; - }); - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let count = 1; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1; - $node1 = new $$PropView($addUpdate => { - let $node0; - $node0 = $$createElement("div"); - $node0.textContent = "1"; - return [$node0]; - }); - $node0 = $$Comp(Child, { - "className": count, - "children": $node1 - }); - return [[$node0], $changed => { - if ($changed & 1) { - $node0 && $node0._$setProp("className", () => count, [count]); - } - $node1 && $node1.update($changed); - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from './mock'; + +describe('generate', () => { + it('should generate createComponent', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let count = 1; + + return +
    1
    +
    ; + }); + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let count = 1; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1; + $node1 = $$createNode(6 /*Children*/, $addUpdate => { + let $node0; + $node0 = $$createElement("div"); + $node0.textContent = "1"; + return [$node0]; + }); + $node0 = $$Comp(Child, { + "className": count, + "children": $node1 + }); + return [[$node0], $changed => { + if ($changed & 1) { + $node0 && $$setProp($node0, "className", () => count, [count]); + } + $$updateNode($node1, $changed); + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/generator/generator.test.tsx b/packages/transpiler/babel-inula-next-core/test/generator/generator.test.tsx index 8a6bd893c321c6f0ee2b3d13741d67e2fe491260..ff92a626cd8f9fb4538fad5ae23f6b56efd552fc 100644 --- a/packages/transpiler/babel-inula-next-core/test/generator/generator.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/generator/generator.test.tsx @@ -1,277 +1,277 @@ -import { describe, expect, it } from 'vitest'; -import { transform } from './mock'; - -describe('generate', () => { - it('should generate createComponent', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let count = 1 - - return <> - }) - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let count = 1; - self = $$createComponent({ - updateState: changed => {} - }); - return self.init(); - }" - `); - }); - - it('should collect lifecycle', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let count = 1 - - willMount(() => { - console.log('will mount') - }) - - console.log(count) - - willMount(() => { - console.log('will mount2') - }) - - didMount(() => { - console.log('mounted') - }) - - willUnmount(() => { - console.log('unmounted') - }) - - return <> - }) - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let count = 1; - self = $$createComponent({ - didMount: () => { - { - console.log('mounted'); - } - }, - willUnmount: () => { - { - console.log('unmounted'); - } - }, - updateState: changed => {}, - getUpdateViews: () => { - { - console.log('will mount'); - } - console.log(count); - { - console.log('will mount2'); - } - return [[],,]; - } - }); - return self.init(); - }" - `); - }); - - it('should generate updateState', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let count = 1 - let doubleCount = count * 2 - - return <> - }) - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let count = 1; - let doubleCount; - self = $$createComponent({ - updateState: changed => { - if (changed & 1) { - if ($$notCached(self, Symbol.for("inula-cache"), [count])) { - doubleCount = count * 2; - } - } - } - }); - return self.init(); - }" - `); - }); - - it('should generate updateState with multiple states', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let count = 1 - let doubleCount = count * 2 - let ff = count * 2 - let nn = [] - let kk = count * doubleCount + 100 + nn[1] - - watch(() => { - let nono = 1 - console.log(count) - }) - watch(() => { - let nono = 2 - console.log(count) - }) - nn.push("jj") - - ;[count, doubleCount] = (() => {nn.push("nn")}) - - return <> - }) - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let count = 1; - let doubleCount; - let ff; - let nn = []; - let kk; - self = $$createComponent({ - updateState: changed => { - if (changed & 1) { - if ($$notCached(self, Symbol.for("inula-cache"), [count])) { - self.updateDerived(doubleCount = count * 2, 2 /*0b10*/); - ff = count * 2; - { - let nono = 1; - console.log(count); - } - { - let nono = 2; - console.log(count); - } - } - } - if (changed & 7) { - if ($$notCached(self, Symbol.for("inula-cache"), [count, doubleCount, nn])) { - kk = count * doubleCount + 100 + nn[1]; - } - } - }, - getUpdateViews: () => { - self.updateDerived(nn.push("jj"), 4 /*0b100*/); - self.updateDerived([count, doubleCount] = () => { - self.updateDerived(nn.push("nn"), 4 /*0b100*/); - }, 3 /*0b11*/); - return [[],,]; - } - }); - return self.init(); - }" - `); - }); - - it('should generate updateProp', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let prop1_$p$_ = 1 - - return <> - }) - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let prop1_$p$_ = 1; - self = $$createComponent({ - updateState: changed => {}, - updateProp: (propName, newValue) => { - if (propName === "prop1") { - prop1_$p$_ = newValue; - } - } - }); - return self.init(); - }" - `); - }); - - it('should generate updateProp with multiple props', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let prop1_$p$_ = 1 - let prop2_$p$_ = 1 - - return <> - }) - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let prop1_$p$_ = 1; - let prop2_$p$_ = 1; - self = $$createComponent({ - updateState: changed => {}, - updateProp: (propName, newValue) => { - if (propName === "prop2") { - prop2_$p$_ = newValue; - } else if (propName === "prop1") { - prop1_$p$_ = newValue; - } - } - }); - return self.init(); - }" - `); - }); - - it('should generate updateProp with updateDerived', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let prop1_$p$_ = 1 - let derived = prop1_$p$_ * 2 - watch(() => { - console.log(prop1_$p$_) - }) - - return <> - }) - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let prop1_$p$_ = 1; - let derived; - self = $$createComponent({ - updateState: changed => { - if (changed & 1) { - if ($$notCached(self, Symbol.for("inula-cache"), [prop1_$p$_])) { - derived = prop1_$p$_ * 2; - { - console.log(prop1_$p$_); - } - } - } - }, - updateProp: (propName, newValue) => { - if (propName === "prop1") { - self.updateDerived(prop1_$p$_ = newValue, 1 /*0b1*/); - } - } - }); - return self.init(); - }" - `); - }); -}); +import { describe, expect, it } from 'vitest'; +import { transform } from './mock'; + +describe('generate', () => { + it('should generate createComponent', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let count = 1 + + return <> + }) + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let count = 1; + self = $$createComponent({ + updateState: changed => {} + }); + return $$initCompNode(self); + }" + `); + }); + + it('should collect lifecycle', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let count = 1 + + willMount(() => { + console.log('will mount') + }) + + console.log(count) + + willMount(() => { + console.log('will mount2') + }) + + didMount(() => { + console.log('mounted') + }) + + willUnmount(() => { + console.log('unmounted') + }) + + return <> + }) + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let count = 1; + self = $$createComponent({ + didMount: () => { + { + console.log('mounted'); + } + }, + willUnmount: () => { + { + console.log('unmounted'); + } + }, + updateState: changed => {}, + getUpdateViews: () => { + { + console.log('will mount'); + } + console.log(count); + { + console.log('will mount2'); + } + return [[],,]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should generate updateState', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let count = 1 + let doubleCount = count * 2 + + return <> + }) + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let count = 1; + let doubleCount; + self = $$createComponent({ + updateState: changed => { + if (changed & 1) { + if ($$notCached(self, "cache0", [count])) { + doubleCount = count * 2; + } + } + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should generate updateState with multiple states', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let count = 1 + let doubleCount = count * 2 + let ff = count * 2 + let nn = [] + let kk = count * doubleCount + 100 + nn[1] + + watch(() => { + let nono = 1 + console.log(count) + }) + watch(() => { + let nono = 2 + console.log(count) + }) + nn.push("jj") + + ;[count, doubleCount] = (() => {nn.push("nn")}) + + return <> + }) + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let count = 1; + let doubleCount; + let ff; + let nn = []; + let kk; + self = $$createComponent({ + updateState: changed => { + if (changed & 1) { + if ($$notCached(self, "cache0", [count])) { + $$updateNode(self, doubleCount = count * 2, 2 /*0b10*/); + ff = count * 2; + { + let nono = 1; + console.log(count); + } + { + let nono = 2; + console.log(count); + } + } + } + if (changed & 7) { + if ($$notCached(self, "cache1", [count, doubleCount, nn])) { + kk = count * doubleCount + 100 + nn[1]; + } + } + }, + getUpdateViews: () => { + $$updateNode(self, nn.push("jj"), 4 /*0b100*/); + $$updateNode(self, [count, doubleCount] = () => { + $$updateNode(self, nn.push("nn"), 4 /*0b100*/); + }, 3 /*0b11*/); + return [[],,]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should generate updateProp', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let prop1_$p$_ = 1 + + return <> + }) + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let prop1_$p$_ = 1; + self = $$createComponent({ + updateState: changed => {}, + updateProp: (propName, newValue) => { + if (propName === "prop1") { + prop1_$p$_ = newValue; + } + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should generate updateProp with multiple props', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let prop1_$p$_ = 1 + let prop2_$p$_ = 1 + + return <> + }) + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let prop1_$p$_ = 1; + let prop2_$p$_ = 1; + self = $$createComponent({ + updateState: changed => {}, + updateProp: (propName, newValue) => { + if (propName === "prop2") { + prop2_$p$_ = newValue; + } else if (propName === "prop1") { + prop1_$p$_ = newValue; + } + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should generate updateProp with updateDerived', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let prop1_$p$_ = 1 + let derived = prop1_$p$_ * 2 + watch(() => { + console.log(prop1_$p$_) + }) + + return <> + }) + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let prop1_$p$_ = 1; + let derived; + self = $$createComponent({ + updateState: changed => { + if (changed & 1) { + if ($$notCached(self, "cache0", [prop1_$p$_])) { + derived = prop1_$p$_ * 2; + { + console.log(prop1_$p$_); + } + } + } + }, + updateProp: (propName, newValue) => { + if (propName === "prop1") { + $$updateNode(self, prop1_$p$_ = newValue, 1 /*0b1*/); + } + } + }); + return $$initCompNode(self); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/generator/subCom.test.tsx b/packages/transpiler/babel-inula-next-core/test/generator/subCom.test.tsx index 6f41ebbc453371a6e15c542037c1984854fed668..89dc33729e14d46abeffa14894c3c51d45123c39 100644 --- a/packages/transpiler/babel-inula-next-core/test/generator/subCom.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/generator/subCom.test.tsx @@ -1,66 +1,66 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { transform } from './mock'; - -describe('generate', () => { - it('should generate createComponent', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let count = 1; - function SubComp() { - - } - - return -
    1
    -
    ; - }); - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let count = 1; - function SubComp() {} - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1; - $node1 = new $$PropView($addUpdate => { - let $node0; - $node0 = $$createElement("div"); - $node0.textContent = "1"; - return [$node0]; - }); - $node0 = $$Comp(Child, { - "className": count, - "children": $node1 - }); - return [[$node0], $changed => { - if ($changed & 1) { - $node0 && $node0._$setProp("className", () => count, [count]); - } - $node1 && $node1.update($changed); - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from './mock'; + +describe('generate', () => { + it('should generate createComponent', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let count = 1; + function SubComp() { + + } + + return +
    1
    +
    ; + }); + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let count = 1; + function SubComp() {} + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1; + $node1 = $$createNode(6 /*Children*/, $addUpdate => { + let $node0; + $node0 = $$createElement("div"); + $node0.textContent = "1"; + return [$node0]; + }); + $node0 = $$Comp(Child, { + "className": count, + "children": $node1 + }); + return [[$node0], $changed => { + if ($changed & 1) { + $node0 && $$setProp($node0, "className", () => count, [count]); + } + $$updateNode($node1, $changed); + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/generator/view.test.tsx b/packages/transpiler/babel-inula-next-core/test/generator/view.test.tsx index 225c6324b4c011e0a9eb763b511e5360fa8e3d7b..4ef75afa5590de4d12e11b3d4d267f32b3af7f45 100644 --- a/packages/transpiler/babel-inula-next-core/test/generator/view.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/generator/view.test.tsx @@ -1,322 +1,322 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { transform } from './mock'; -describe('view generation', () => { - it('should generation single html', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let text = 'hello world'; - console.log(text); - return
    {text}
    - }) - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let text = 'hello world'; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - console.log(text); - let $node0, $node1; - $node0 = $$createElement("div"); - $node1 = new $$ExpNode(text, [text]); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [[$node0], $changed => { - if ($changed & 1) { - $node1 && $node1.update(() => text, [text]); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); - - it('should generate html properties and update', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let text = 'hello world'; - let color = 'red'; - - - return
    {text}
    - }) - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let text = 'hello world'; - let color = 'red'; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1; - $node0 = $$createElement("div"); - $$setHTMLProp($node0, "className", () => text, [text]); - $$setHTMLProp($node0, "id", () => text, [text]); - $$setStyle($node0, { - color - }); - $node1 = new $$ExpNode(text, [text]); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [[$node0], $changed => { - if ($changed & 1) { - $node0 && $$setHTMLProp($node0, "className", () => text, [text]); - $node0 && $$setHTMLProp($node0, "id", () => text, [text]); - $node1 && $node1.update(() => text, [text]); - } - if ($changed & 2) { - $node0 && $$setStyle($node0, { - color - }); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); - - it('should generate multiple html', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let text = 'hello world'; - return ( -
    -
    {text}
    -
    {text}
    -
    - ) - }) - `); - - expect(code).toMatchInlineSnapshot(` - "const _$t = (() => { - let $node0, $node1, $node2; - $node0 = $$createElement("div"); - $node1 = $$createElement("div"); - $node0.appendChild($node1); - $node2 = $$createElement("div"); - $node0.appendChild($node2); - return $node0; - })(); - function Comp() { - let self; - let text = 'hello world'; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1, $node2, $node3, $node4; - $node0 = _$t.cloneNode(true); - $node1 = $node0.firstChild; - $node2 = $node1.nextSibling; - $node3 = new $$ExpNode(text, [text]); - $$insertNode($node1, $node3, 0); - $node4 = new $$ExpNode(text, [text]); - $$insertNode($node2, $node4, 0); - return [[$node0], $changed => { - if ($changed & 1) { - $node3 && $node3.update(() => text, [text]); - $node4 && $node4.update(() => text, [text]); - } - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); - - it('should support fragment', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let text = 'hello world'; - return ( - <> -
    {text}
    -
    {text}
    - - ) - }) - `); - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let text = 'hello world'; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1, $node2, $node3; - $node0 = $$createElement("div"); - $node1 = new $$ExpNode(text, [text]); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - $node2 = $$createElement("div"); - $node3 = new $$ExpNode(text, [text]); - $$insertNode($node2, $node3, 0); - $node2._$nodes = [$node3]; - return [[$node0, $node2], $changed => { - if ($changed & 1) { - $node1 && $node1.update(() => text, [text]); - $node3 && $node3.update(() => text, [text]); - } - return [$node0, $node2]; - }]; - } - }); - return self.init(); - }" - `); - }); - it('should generate conditional html', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let text = 'hello world'; - let show = true; - return ( -
    - -
    {text}
    -
    - -

    else

    -
    -
    - ); - }); - `); - - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let text = 'hello world'; - let show = true; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1; - $node0 = $$createElement("div"); - $node1 = new $$CondNode(2, $thisCond => { - if (show) { - if ($thisCond.cond === 0) { - $thisCond.didntChange = true; - return []; - } - $thisCond.cond = 0; - let $node0, $node1; - $thisCond.updateFunc = $changed => { - if ($changed & 1) { - $node1 && $node1.update(() => text, [text]); - } - }; - $node0 = $$createElement("div"); - $node1 = new $$ExpNode(text, [text]); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return $thisCond.cond === 0 ? [$node0] : $thisCond.updateCond(); - } else { - if ($thisCond.cond === 1) { - $thisCond.didntChange = true; - return []; - } - $thisCond.cond = 1; - let $node0; - $thisCond.updateFunc = $changed => {}; - $node0 = $$createElement("h1"); - $node0.textContent = "else"; - return $thisCond.cond === 1 ? [$node0] : $thisCond.updateCond(); - } - }); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [[$node0], $changed => { - if ($changed & 2) { - $node1 && $node1.updateCond(); - } - $node1 && $node1.update($changed); - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); - - it('should generate loop html', () => { - const code = transform(/*js*/ ` - const Comp = Component(() => { - let list = ['hello', 'world']; - return ( -
    - - {(item, index) =>
    {item}
    })} -
    -
    - ); - }); - `); - expect(code).toMatchInlineSnapshot(` - "function Comp() { - let self; - let list = ['hello', 'world']; - self = $$createComponent({ - updateState: changed => {}, - getUpdateViews: () => { - let $node0, $node1; - $node0 = $$createElement("div"); - $node1 = new $$ForNode(list, 1, list.map(item => index), (item, index, $updateArr) => { - let $node0, $node1; - $updateArr[index] = ($changed, $item) => { - item = $item; - if ($changed & 1) { - $node1 && $node1.update(() => item, [item]); - } - }; - $node0 = $$createElement("div"); - $node0.setAttribute("key", index); - $node1 = new $$ExpNode(item, [item]); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [$node0]; - }); - $$insertNode($node0, $node1, 0); - $node0._$nodes = [$node1]; - return [[$node0], $changed => { - if ($changed & 1) { - $node1 && $node1.updateArray(list, list.map(item => index)); - } - $node1 && $node1.update($changed); - return [$node0]; - }]; - } - }); - return self.init(); - }" - `); - }); -}); +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { describe, expect, it } from 'vitest'; +import { transform } from './mock'; +describe('view generation', () => { + it('should generation single html', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let text = 'hello world'; + console.log(text); + return
    {text}
    + }) + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let text = 'hello world'; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + console.log(text); + let $node0, $node1; + $node0 = $$createElement("div"); + $node1 = $$createNode(3 /*Exp*/, () => text, [text]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [[$node0], $changed => { + if ($changed & 1) { + $node1 && $$updateNode($node1, () => text, [text]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should generate html properties and update', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let text = 'hello world'; + let color = 'red'; + + + return
    {text}
    + }) + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let text = 'hello world'; + let color = 'red'; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1; + $node0 = $$createElement("div"); + $$setHTMLProp($node0, "className", () => text, [text]); + $$setHTMLProp($node0, "id", () => text, [text]); + $$setStyle($node0, { + color + }); + $node1 = $$createNode(3 /*Exp*/, () => text, [text]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [[$node0], $changed => { + if ($changed & 1) { + $node0 && $$setHTMLProp($node0, "className", () => text, [text]); + $node0 && $$setHTMLProp($node0, "id", () => text, [text]); + $node1 && $$updateNode($node1, () => text, [text]); + } + if ($changed & 2) { + $node0 && $$setStyle($node0, { + color + }); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should generate multiple html', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let text = 'hello world'; + return ( +
    +
    {text}
    +
    {text}
    +
    + ) + }) + `); + + expect(code).toMatchInlineSnapshot(` + "const _$t = (() => { + let $node0, $node1, $node2; + $node0 = $$createElement("div"); + $node1 = $$createElement("div"); + $$appendNode($node0, $node1); + $node2 = $$createElement("div"); + $$appendNode($node0, $node2); + return $node0; + })(); + function Comp() { + let self; + let text = 'hello world'; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1, $node2, $node3, $node4; + $node0 = _$t.cloneNode(true); + $node1 = $node0.firstChild; + $node2 = $node1.nextSibling; + $node3 = $$createNode(3 /*Exp*/, () => text, [text]); + $$insertNode($node1, $node3, 0); + $node4 = $$createNode(3 /*Exp*/, () => text, [text]); + $$insertNode($node2, $node4, 0); + return [[$node0], $changed => { + if ($changed & 1) { + $node3 && $$updateNode($node3, () => text, [text]); + $node4 && $$updateNode($node4, () => text, [text]); + } + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should support fragment', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let text = 'hello world'; + return ( + <> +
    {text}
    +
    {text}
    + + ) + }) + `); + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let text = 'hello world'; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1, $node2, $node3; + $node0 = $$createElement("div"); + $node1 = $$createNode(3 /*Exp*/, () => text, [text]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + $node2 = $$createElement("div"); + $node3 = $$createNode(3 /*Exp*/, () => text, [text]); + $$insertNode($node2, $node3, 0); + $node2._$nodes = [$node3]; + return [[$node0, $node2], $changed => { + if ($changed & 1) { + $node1 && $$updateNode($node1, () => text, [text]); + $node3 && $$updateNode($node3, () => text, [text]); + } + return [$node0, $node2]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + it('should generate conditional html', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let text = 'hello world'; + let show = true; + return ( +
    + +
    {text}
    +
    + +

    else

    +
    +
    + ); + }); + `); + + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let text = 'hello world'; + let show = true; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1; + $node0 = $$createElement("div"); + $node1 = $$createNode(2 /*Cond*/, 2, $thisCond => { + if (show) { + if ($thisCond.cond === 0) { + $thisCond.didntChange = true; + return []; + } + $thisCond.cond = 0; + let $node0, $node1; + $thisCond.updateFunc = $changed => { + if ($changed & 1) { + $node1 && $$updateNode($node1, () => text, [text]); + } + }; + $node0 = $$createElement("div"); + $node1 = $$createNode(3 /*Exp*/, () => text, [text]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return $thisCond.cond === 0 ? [$node0] : $$updateNode($thisCond); + } else { + if ($thisCond.cond === 1) { + $thisCond.didntChange = true; + return []; + } + $thisCond.cond = 1; + let $node0; + $thisCond.updateFunc = $changed => {}; + $node0 = $$createElement("h1"); + $node0.textContent = "else"; + return $thisCond.cond === 1 ? [$node0] : $$updateNode($thisCond); + } + }); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [[$node0], $changed => { + if ($changed & 2) { + $node1 && $$updateNode($node1); + } + $node1 && $$updateChildren($node1, $changed); + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); + + it('should generate loop html', () => { + const code = transform(/*js*/ ` + const Comp = Component(() => { + let list = ['hello', 'world']; + return ( +
    + + {(item, index) =>
    {item}
    })} +
    +
    + ); + }); + `); + expect(code).toMatchInlineSnapshot(` + "function Comp() { + let self; + let list = ['hello', 'world']; + self = $$createComponent({ + updateState: changed => {}, + getUpdateViews: () => { + let $node0, $node1; + $node0 = $$createElement("div"); + $node1 = $$createNode(1 /*For*/, list, 1, list.map(item => index), (item, index, $updateArr) => { + let $node0, $node1; + $updateArr[index] = ($changed, $item) => { + item = $item; + if ($changed & 1) { + $node1 && $$updateNode($node1, () => item, [item]); + } + }; + $node0 = $$createElement("div"); + $node0.setAttribute("key", index); + $node1 = $$createNode(3 /*Exp*/, () => item, [item]); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [$node0]; + }); + $$insertNode($node0, $node1, 0); + $node0._$nodes = [$node1]; + return [[$node0], $changed => { + if ($changed & 1) { + $node1 && $$updateNode($node1, list, list.map(item => index)); + } + $node1 && $$updateChildren($node1, $changed); + return [$node0]; + }]; + } + }); + return $$initCompNode(self); + }" + `); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/mock.ts b/packages/transpiler/babel-inula-next-core/test/mock.ts index 50c846c2ef38d6ece1490fd356f8c113d90c81c3..31d76a2ed5c6e3a3f9bfbb4612ab6968753362b2 100644 --- a/packages/transpiler/babel-inula-next-core/test/mock.ts +++ b/packages/transpiler/babel-inula-next-core/test/mock.ts @@ -1,33 +1,33 @@ -/* - * 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 { transform as transformWithBabel } from '@babel/core'; -import generate from '@babel/generator'; -import { types as t } from '@openinula/babel-api'; -import plugin from '../src'; - -export function genCode(ast: t.Node | null) { - if (!ast) { - throw new Error('ast is null'); - } - return generate(ast).code; -} - -export function transform(code: string) { - return transformWithBabel(code, { - presets: [plugin], - filename: 'test.tsx', - })?.code; -} +/* + * 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 { transform as transformWithBabel } from '@babel/core'; +import generate from '@babel/generator'; +import { types as t } from '@openinula/babel-api'; +import plugin from '../src'; + +export function genCode(ast: t.Node | null) { + if (!ast) { + throw new Error('ast is null'); + } + return generate(ast).code; +} + +export function transform(code: string) { + return transformWithBabel(code, { + presets: [plugin], + filename: 'test.tsx', + })?.code; +} 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 index bb02988226d6cf519f7e396cc0820beffc3990f4..4c48c9fe265a6d8757b8ebf0816c77588b01c71e 100644 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/autoNaming.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/autoNaming.test.tsx @@ -1,128 +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 = () => {};"`); - }); - }); -}); +/* + * 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/contextPlugin.test.tsx b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/contextPlugin.test.tsx index e497f287a86cb84fd55ac08dbb4ebaab7365ec27..11c21ccfb9403378f488a29dacb3a348b1493e5e 100644 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/contextPlugin.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/contextPlugin.test.tsx @@ -1,113 +1,113 @@ -/* - * 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 contextPlugin from '../../src/sugarPlugins/contextPlugin'; -import autoNamingPlugin from '../../src/sugarPlugins/autoNamingPlugin'; - -const mock = (code: string) => compile([autoNamingPlugin, contextPlugin], code); - -describe('useContext transform', () => { - describe('object destructuring', () => { - it('should transform object destructuring useContext call', () => { - const code = ` - function App() { - const { level, path } = useContext(UserContext); - console.log(level, path); - } - `; - const transformedCode = mock(code); - expect(transformedCode).toMatchInlineSnapshot(` - "const App = Component(() => { - let path_$c$_ = useContext(UserContext, "path"); - let level_$c$_ = useContext(UserContext, "level"); - console.log(level_$c$_, path_$c$_); - });" - `); - }); - - it.fails('should transform nested object destructuring', () => { - const code = ` - function App() { - const { user: { name, age } } = useContext(UserContext); - const { name, age } = user; - } - `; - const transformedCode = mock(code); - expect(transformedCode).toMatchInlineSnapshot(` - "const App = Component(() => { - let user_$c$_ = useContext(UserContext, "user"); - });" - `); - }); - }); - - describe('direct assignment', () => { - it('should transform direct assignment useContext call', () => { - const code = ` - function App() { - const user = useContext(UserContext); - console.log(user); - } - `; - const transformedCode = mock(code); - expect(transformedCode).toMatchInlineSnapshot(` - "const App = Component(() => { - let user_$ctx$_ = useContext(UserContext); - console.log(user_$ctx$_); - });" - `); - }); - }); - - describe('invalid usage', () => { - it('should throw error for useContext not in variable declaration', () => { - const code = ` - function App() { - const a = true && useContext(UserContext); - } - `; - expect(() => mock(code)).toThrow(); - }); - - it('should throw error for useContext with computed context', () => { - const code = ` - function App() { - const value = useContext(getContext()); - } - `; - expect(() => mock(code)).toThrow(); - }); - }); - - describe('multiple useContext calls', () => { - it('should transform multiple useContext calls', () => { - const code = ` - function App() { - const { theme } = useContext(ThemeContext); - const user = useContext(UserContext); - } - `; - const transformedCode = mock(code); - expect(transformedCode).toMatchInlineSnapshot(` - "const App = Component(() => { - let theme_$c$_ = useContext(ThemeContext, "theme"); - let user_$ctx$_ = useContext(UserContext); - });" - `); - }); - }); -}); +/* + * 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 contextPlugin from '../../src/sugarPlugins/contextPlugin'; +import autoNamingPlugin from '../../src/sugarPlugins/autoNamingPlugin'; + +const mock = (code: string) => compile([autoNamingPlugin, contextPlugin], code); + +describe('useContext transform', () => { + describe('object destructuring', () => { + it('should transform object destructuring useContext call', () => { + const code = ` + function App() { + const { level, path } = useContext(UserContext); + console.log(level, path); + } + `; + const transformedCode = mock(code); + expect(transformedCode).toMatchInlineSnapshot(` + "const App = Component(() => { + let path_$c$_ = useContext(UserContext, "path"); + let level_$c$_ = useContext(UserContext, "level"); + console.log(level_$c$_, path_$c$_); + });" + `); + }); + + it.fails('should transform nested object destructuring', () => { + const code = ` + function App() { + const { user: { name, age } } = useContext(UserContext); + const { name, age } = user; + } + `; + const transformedCode = mock(code); + expect(transformedCode).toMatchInlineSnapshot(` + "const App = Component(() => { + let user_$c$_ = useContext(UserContext, "user"); + });" + `); + }); + }); + + describe('direct assignment', () => { + it('should transform direct assignment useContext call', () => { + const code = ` + function App() { + const user = useContext(UserContext); + console.log(user); + } + `; + const transformedCode = mock(code); + expect(transformedCode).toMatchInlineSnapshot(` + "const App = Component(() => { + let user_$ctx$_ = useContext(UserContext); + console.log(user_$ctx$_); + });" + `); + }); + }); + + describe('invalid usage', () => { + it('should throw error for useContext not in variable declaration', () => { + const code = ` + function App() { + const a = true && useContext(UserContext); + } + `; + expect(() => mock(code)).toThrow(); + }); + + it('should throw error for useContext with computed context', () => { + const code = ` + function App() { + const value = useContext(getContext()); + } + `; + expect(() => mock(code)).toThrow(); + }); + }); + + describe('multiple useContext calls', () => { + it('should transform multiple useContext calls', () => { + const code = ` + function App() { + const { theme } = useContext(ThemeContext); + const user = useContext(UserContext); + } + `; + const transformedCode = mock(code); + expect(transformedCode).toMatchInlineSnapshot(` + "const App = Component(() => { + let theme_$c$_ = useContext(ThemeContext, "theme"); + let user_$ctx$_ = useContext(UserContext); + });" + `); + }); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/destructuring.test.tsx b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/destructuring.test.tsx index 0c5454bc2216e7dab2396253eeaa2edf38739243..5551c79962f018bbf228b47efe2fec08f7dc7df2 100644 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/destructuring.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/destructuring.test.tsx @@ -1,96 +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/stateDestructuringPlugin'; - -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); - }); - });" - `); - }); -}); +/* + * 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/stateDestructuringPlugin'; + +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/forSubComponent.test.tsx b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/forSubComponent.test.tsx index 19892ed6043b0343a68911cd5c1992a55a97752d..5f3a51b0adcbafb78c5198ad1d725056a15299cc 100644 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/forSubComponent.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/forSubComponent.test.tsx @@ -1,100 +1,100 @@ -/* - * 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 forSubComponentPlugin from '../../src/sugarPlugins/forSubComponentPlugin'; - -const mock = (code: string) => compile([forSubComponentPlugin], code); -describe('for', () => { - it('should transform for loop', () => { - const code = ` - function MyComp() { - let name = 'test'; - let arr = [{x:1, y:1}, {x:2, y:2}, {x:3, y:3}] - return ( -
    - {({x, y}, index) => { - let name1 = 'test' - const onClick = () => { - name1 = 'test2' - } - - return
    {name1}
    - }}
    - {({x, y}, index) => { - let name1 = 'test' - const onClick = () => { - name1 = 'test2' - } - - return
    {name1}
    - }}
    -
    - ) - } - `; - const transformedCode = mock(code); - expect(transformedCode).toMatchInlineSnapshot(`"function MyComp() { - let name = 'test'; - let arr = [{ - x: 1, - y: 1 - }, { - x: 2, - y: 2 - }, { - x: 3, - y: 3 - }]; - function For_1({ - x, - y, - index - }) { - let name1 = 'test'; - const onClick = () => { - name1 = 'test2'; - }; - return
    {name1}
    ; - } - function For_2({ - x, - y, - index - }) { - let name1 = 'test'; - const onClick = () => { - name1 = 'test2'; - }; - return
    {name1}
    ; - } - return
    - {({ - x, - y - }, index) => } - {({ - x, - y - }, index) => } -
    ; -}"`); - }); -}); +/* + * 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 forSubComponentPlugin from '../../src/sugarPlugins/forSubComponentPlugin'; + +const mock = (code: string) => compile([forSubComponentPlugin], code); +describe('for', () => { + it('should transform for loop', () => { + const code = ` + function MyComp() { + let name = 'test'; + let arr = [{x:1, y:1}, {x:2, y:2}, {x:3, y:3}] + return ( +
    + {({x, y}, index) => { + let name1 = 'test' + const onClick = () => { + name1 = 'test2' + } + + return
    {name1}
    + }}
    + {({x, y}, index) => { + let name1 = 'test' + const onClick = () => { + name1 = 'test2' + } + + return
    {name1}
    + }}
    +
    + ) + } + `; + const transformedCode = mock(code); + expect(transformedCode).toMatchInlineSnapshot(`"function MyComp() { + let name = 'test'; + let arr = [{ + x: 1, + y: 1 + }, { + x: 2, + y: 2 + }, { + x: 3, + y: 3 + }]; + function For_1({ + x, + y, + index + }) { + let name1 = 'test'; + const onClick = () => { + name1 = 'test2'; + }; + return
    {name1}
    ; + } + function For_2({ + x, + y, + index + }) { + let name1 = 'test'; + const onClick = () => { + name1 = 'test2'; + }; + return
    {name1}
    ; + } + return
    + {({ + x, + y + }, index) => } + {({ + x, + y + }, index) => } +
    ; +}"`); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/hookPlugin.test.tsx b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/hookPlugin.test.tsx index d1458275ce0ad71bc95a5dcd97b245153c20cc8f..136e63f7f1ac2edec5c3ee43c836a13601caca0a 100644 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/hookPlugin.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/hookPlugin.test.tsx @@ -1,69 +1,82 @@ -/* - * Copyright (c) 2024 Huawei Technologies Co.,Ltd. - * - * openInula is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -import { describe, expect, it } from 'vitest'; -import { compile } from './mock'; -import autoNamingPlugin from '../../src/sugarPlugins/autoNamingPlugin'; -import hookPlugin from '../../src/sugarPlugins/hookPlugin'; - -const mock = (code: string) => compile([autoNamingPlugin, hookPlugin], code); - -describe('use hook transform', () => { - it('should transform a simple custom hook call', () => { - const input = ` - function App() { - const [x, y] = useMousePosition(0, 0); - } - `; - const output = mock(input); - expect(output).toMatchInlineSnapshot(` - "const App = Component(() => { - let _useMousePosition_$h$_ = $$useHook(useMousePosition, [0, 0]); - const [x, y] = _useMousePosition_$h$_.value(); - });" - `); - }); - - it('should transform a custom hook call with multiple params', () => { - const input = ` - function App() { - let baseX = 0; - let baseY = 0; - const [x, y] = useMousePosition(baseX, baseY); - watch(() => { - console.log(baseX, baseY) - }) - } - `; - const output = mock(input); - expect(output).toMatchInlineSnapshot(` - "const App = Component(() => { - let baseX = 0; - let baseY = 0; - let _useMousePosition_$h$_ = $$useHook(useMousePosition, [baseX, baseY]); - watch(() => { - $$untrack(() => _useMousePosition_$h$_).updateProp("p0", baseX); - }); - watch(() => { - $$untrack(() => _useMousePosition_$h$_).updateProp("p1", baseY); - }); - const [x, y] = _useMousePosition_$h$_.value(); - watch(() => { - console.log(baseX, baseY); - }); - });" - `); - }); -}); +/* + * 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'; +import hookPlugin from '../../src/sugarPlugins/hookPlugin'; + +const mock = (code: string) => compile([autoNamingPlugin, hookPlugin], code); + +describe('use hook transform', () => { + it('should transform a simple custom hook call', () => { + const input = ` + function App() { + const [x, y] = useMousePosition(0, 0); + } + `; + const output = mock(input); + expect(output).toMatchInlineSnapshot(` + "const App = Component(() => { + let _useMousePosition_$h$_ = $$useHook(useMousePosition, [0, 0]); + const [x, y] = _useMousePosition_$h$_.value(); + });" + `); + }); + + it('should transform a custom hook call with multiple params', () => { + const input = ` + function App() { + let baseX = 0; + let baseY = 0; + const [x, y] = useMousePosition(baseX, baseY); + watch(() => { + console.log(baseX, baseY) + }) + } + `; + const output = mock(input); + expect(output).toMatchInlineSnapshot(` + "const App = Component(() => { + let baseX = 0; + let baseY = 0; + let _useMousePosition_$h$_ = $$useHook(useMousePosition, [baseX, baseY]); + watch(() => { + $$untrack(() => _useMousePosition_$h$_) && $$untrack(() => _useMousePosition_$h$_).updateProp("p0", baseX); + }); + watch(() => { + $$untrack(() => _useMousePosition_$h$_) && $$untrack(() => _useMousePosition_$h$_).updateProp("p1", baseY); + }); + const [x, y] = _useMousePosition_$h$_.value(); + watch(() => { + console.log(baseX, baseY); + }); + });" + `); + }); + + it('should transform hook props', () => { + const input = ` + function useMousePosition(baseX,baseY) {} + `; + const output = mock(input); + expect(output).toMatchInlineSnapshot(` + "const useMousePosition = Hook(({ + p0: baseX, + p1: baseY + }) => {});" + `); + }); +}); 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 index 46c77db54b49b1d068ba989b76e6bdea67ced27a..a453b8f2dcc43a981e725ad35c7a3c72769e9938 100644 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.ts @@ -1,142 +1,142 @@ -/* - * 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 JSX_div = Component(() =>
    ); - const a = $$Comp(JSX_div); - }" - `); - }); - - it('should support multi level jsx', () => { - expect( - mock(` - function App() { - const content =
    -
    -

    Title

    -
    -
    -

    Content

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

    Title

    -
    -
    -

    Content

    -
    -
    ); - const content = $$Comp(JSX_div); - }" - `); - }); - 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 ? $$Comp(JSX_Table_Col) : $$Comp(JSX_div); - }" - `); - }); - - 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 = [$$Comp(JSX_div), $$Comp(JSX_h)]; - }" - `); - }); - - it('should work with jsx slice in jsx attribute', () => { - expect( - mock(` - function App() { - return
    }>
    - } - `) - ).toMatchInlineSnapshot(` - "function App() { - const JSX_Icon = Component(() => ); - return
    ; - }" - `); - }); - - it('fragment should work', () => { - expect( - mock(` - function App() { - const a = <>{test} - const b = cond ? <>
    : <> - } - `) - ).toMatchInlineSnapshot(` - "function App() { - const JSX_Fragment = Component(() => <>{test}); - const a = $$Comp(JSX_Fragment); - const JSX_Fragment2 = Component(() => <>
    ); - const JSX_Fragment3 = Component(() => <>); - const b = cond ? $$Comp(JSX_Fragment2) : $$Comp(JSX_Fragment3); - }" - `); - }); - - // TODO: Fix this test - it.skip('should work with jsx slice in function', () => { - // function App() { - // const fn = ([x, y, z]) => { - // return
    {x}, {y}, {z}
    - // } - // - // return
    {fn([1, 2, 3])}
    - // } - }); -}); +/* + * 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 JSX_div = Component(() =>
    ); + const a = $$Comp(JSX_div); + }" + `); + }); + + it('should support multi level jsx', () => { + expect( + mock(` + function App() { + const content =
    +
    +

    Title

    +
    +
    +

    Content

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

    Title

    +
    +
    +

    Content

    +
    +
    ); + const content = $$Comp(JSX_div); + }" + `); + }); + 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 ? $$Comp(JSX_Table_Col) : $$Comp(JSX_div); + }" + `); + }); + + 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 = [$$Comp(JSX_div), $$Comp(JSX_h)]; + }" + `); + }); + + it('should work with jsx slice in jsx attribute', () => { + expect( + mock(` + function App() { + return
    }>
    + } + `) + ).toMatchInlineSnapshot(` + "function App() { + const JSX_Icon = Component(() => ); + return
    ; + }" + `); + }); + + it('fragment should work', () => { + expect( + mock(` + function App() { + const a = <>{test} + const b = cond ? <>
    : <> + } + `) + ).toMatchInlineSnapshot(` + "function App() { + const JSX_Fragment = Component(() => <>{test}); + const a = $$Comp(JSX_Fragment); + const JSX_Fragment2 = Component(() => <>
    ); + const JSX_Fragment3 = Component(() => <>); + const b = cond ? $$Comp(JSX_Fragment2) : $$Comp(JSX_Fragment3); + }" + `); + }); + + // TODO: Fix this test + it.skip('should work with jsx slice in function', () => { + // function App() { + // const fn = ([x, y, z]) => { + // return
    {x}, {y}, {z}
    + // } + // + // return
    {fn([1, 2, 3])}
    + // } + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/mappingToFor.test.tsx b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/mappingToFor.test.tsx index c3baf1f9b8d943a8e717f4cf1251ab4c48f20bfe..eb2f9bcdc475bb6788ae7c86ae107f710dde2e86 100644 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/mappingToFor.test.tsx +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/mappingToFor.test.tsx @@ -1,106 +1,108 @@ -/* - * 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 mapping2ForPlugin from '../../src/sugarPlugins/mapping2ForPlugin'; - -const mock = (code: string) => compile([mapping2ForPlugin], code); -describe('mapping2ForPlugin', () => { - it('should transform map to for jsxelement', () => { - const code = ` - function MyComp() { - let arr = [1, 2, 3]; - return ( - <> - {arr.map((item) => (
    {item}
    ))} - - ) - } - `; - const transformedCode = mock(code); - expect(transformedCode).toMatchInlineSnapshot(`"function MyComp() { - let arr = [1, 2, 3]; - return <> - {item =>
    {item}
    }
    - ; -}"`); - }); - it('should transform map to for jsxelement inside div', () => { - const code = ` - function MyComp() { - let arr = [1, 2, 3]; - return ( -
    - {arr.map((item) => (
    {item}
    ))} -
    - ) - } - `; - const transformedCode = mock(code); - expect(transformedCode).toMatchInlineSnapshot(`"function MyComp() { - let arr = [1, 2, 3]; - return
    - {item =>
    {item}
    }
    -
    ; -}"`); - }); - it('should transform last map to for" ', () => { - const code = ` - function MyComp() { - let arr = [1, 2, 3]; - return ( -
    - {arr.map(item =>

    {item}

    ).map((item) => (
    {item}
    ))} -
    - ) - } - `; - const transformedCode = mock(code); - expect(transformedCode).toMatchInlineSnapshot(`"function MyComp() { - let arr = [1, 2, 3]; - return
    -

    {item}

    )}>{item =>
    {item}
    }
    -
    ; -}"`); - }); - it('should transform map in map to for" ', () => { - const code = ` - function MyComp() { - let matrix = [[1, 2], [3, 4]]; - return ( -
    - {matrix.map(arr => arr.map(item =>
    {item}
    ))} -
    - ) - } - `; - const transformedCode = mock(code); - expect(transformedCode).toMatchInlineSnapshot(` - "function MyComp() { - let matrix = [[1, 2], [3, 4]]; - return
    - {arr => {item =>
    {item}
    }
    }
    -
    ; - }" - `); - }); - it('should not transform map not in jsx expression container ', () => { - const code = ` - const a = arr.map(i => i+ 1) `; - const transformedCode = mock(code); - expect(transformedCode).toMatchInlineSnapshot(`"const a = arr.map(i => i + 1);"`); - }); -}); +/* + * 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 mapping2ForPlugin from '../../src/sugarPlugins/mapping2ForPlugin'; + +const mock = (code: string) => compile([mapping2ForPlugin], code); +describe('mapping2ForPlugin', () => { + it('should transform map to for jsxelement', () => { + const code = ` + function MyComp() { + let arr = [1, 2, 3]; + return ( + <> + {arr.map((item, index) => (
    {item}
    ))} + + ) + } + `; + const transformedCode = mock(code); + expect(transformedCode).toMatchInlineSnapshot(` + "function MyComp() { + let arr = [1, 2, 3]; + return <> + {(item, index) =>
    {item}
    }
    + ; + }" + `); + }); + it('should transform map to for jsxelement inside div', () => { + const code = ` + function MyComp() { + let arr = [1, 2, 3]; + return ( +
    + {arr.map((item) => (
    {item}
    ))} +
    + ) + } + `; + const transformedCode = mock(code); + expect(transformedCode).toMatchInlineSnapshot(`"function MyComp() { + let arr = [1, 2, 3]; + return
    + {item =>
    {item}
    }
    +
    ; +}"`); + }); + it('should transform last map to for" ', () => { + const code = ` + function MyComp() { + let arr = [1, 2, 3]; + return ( +
    + {arr.map(item =>

    {item}

    ).map((item) => (
    {item}
    ))} +
    + ) + } + `; + const transformedCode = mock(code); + expect(transformedCode).toMatchInlineSnapshot(`"function MyComp() { + let arr = [1, 2, 3]; + return
    +

    {item}

    )}>{item =>
    {item}
    }
    +
    ; +}"`); + }); + it('should transform map in map to for" ', () => { + const code = ` + function MyComp() { + let matrix = [[1, 2], [3, 4]]; + return ( +
    + {matrix.map(arr => arr.map(item =>
    {item}
    ))} +
    + ) + } + `; + const transformedCode = mock(code); + expect(transformedCode).toMatchInlineSnapshot(` + "function MyComp() { + let matrix = [[1, 2], [3, 4]]; + return
    + {arr => {item =>
    {item}
    }
    }
    +
    ; + }" + `); + }); + it('should not transform map not in jsx expression container ', () => { + const code = ` + const a = arr.map(i => i+ 1) `; + const transformedCode = mock(code); + expect(transformedCode).toMatchInlineSnapshot(`"const a = arr.map(i => i + 1);"`); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/mock.ts b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/mock.ts index 9e6793b75e960434f68c203255aea4c7772b6934..844b357de6a73a23ed6160f5a493deadca68f9ea 100644 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/mock.ts +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/mock.ts @@ -1,32 +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; -} +/* + * 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 index 6256607ededea21c5e392bbb2e8754fd059a3864..e151d5e31fda980b5c30b6c4a6c80a5c843de83b 100644 --- a/packages/transpiler/babel-inula-next-core/test/sugarPlugins/props.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/sugarPlugins/props.test.ts @@ -1,227 +1,194 @@ -/* - * 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}) => { - const v = foo + bar; - }) - `) - ).toMatchInlineSnapshot(` - "Component(({ - foo, - bar - }) => { - let foo_$p$_ = foo; - let bar_$p$_ = bar; - const v = foo_$p$_ + bar_$p$_; - });" - `); - }); - - it('should support default value', () => { - expect( - mock(` - Component(({foo = 'default', bar = 123}) => {}) - `) - ).toMatchInlineSnapshot(` - "Component(({ - foo = 'default', - bar = 123 - }) => { - let foo_$p$_ = foo; - let bar_$p$_ = bar; - });" - `); - }); - - it('should support alias', () => { - expect( - mock(/*js*/ ` - Component(({'foo': renamed, bar: anotherName}) => {}) - `) - ).toMatchInlineSnapshot(` - "Component(({ - foo, - bar - }) => { - let foo_$p$_ = foo; - let renamed = foo_$p$_; - let bar_$p$_ = bar; - let anotherName = bar_$p$_; - });" - `); - }); - - it('should support nested props', () => { - expect( - mock(/*js*/ ` - Component(({foo: {nested1, nested2}, bar}) => {}) - `) - ).toMatchInlineSnapshot(` - "Component(({ - foo, - bar - }) => { - let foo_$p$_ = foo; - let { - nested1, - nested2 - } = foo_$p$_; - let bar_$p$_ = bar; - });" - `); - }); - // - 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 ({ - prop1, - prop2 - }) { - let prop1_$p$_ = prop1; - let prop2_$p$_ = prop2; - let { - p2: [p20X = defaultVal, { - p211, - p212: p212X = defaultVal - }, ...restArr], - p3, - ...restObj - } = prop2_$p$_; - });" - `); - }); - - it('should support rest element', () => { - expect( - mock(/*js*/ ` - Component(({foo, ...rest}) => {}) - `) - ).toMatchInlineSnapshot(` - "Component(({ - foo, - ...rest - }) => { - let foo_$p$_ = foo; - let rest_$p$_ = rest; - });" - `); - }); - - 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(({ - foo, - bar - }) => { - let foo_$p$_ = foo; - let bar_$p$_ = bar; - });" - `); - }); - - it('should support props renaming', () => { - expect( - mock(/*js*/ ` - Component((props) => { - const newProps = props; - const {foo: renamed, bar} = newProps; - }) - `) - ).toMatchInlineSnapshot(` - "Component(({ - foo, - bar - }) => { - let foo_$p$_ = foo; - let renamed = foo_$p$_; - let bar_$p$_ = bar; - });" - `); - }); - - it('should support props member expression', () => { - expect( - mock(/*js*/ ` - Component((props) => { - const foo = props.foo; - }) - `) - ).toMatchInlineSnapshot(` - "Component(({ - foo - }) => { - let foo_$p$_ = foo; - });" - `); - }); - it('should support props member expression rename', () => { - expect( - mock(/*js*/ ` - Component((props) => { - const renamed = props.foo - return props.foo; - }) - `) - ).toMatchInlineSnapshot(` - "Component(({ - foo - }) => { - let foo_$p$_ = foo; - let renamed = foo_$p$_; - });" - `); - }); - }); -}); +/* + * 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}) => { + const v = foo + bar; + }) + `) + ).toMatchInlineSnapshot(` + "Component(({ + foo, + bar + }) => { + let foo_$p$_ = foo; + let bar_$p$_ = bar; + const v = foo_$p$_ + bar_$p$_; + });" + `); + }); + + it('should support default value', () => { + expect( + mock(` + Component(({foo = 'default', bar = 123}) => {}) + `) + ).toMatchInlineSnapshot(` + "Component(({ + foo = 'default', + bar = 123 + }) => { + let foo_$p$_ = foo; + let bar_$p$_ = bar; + });" + `); + }); + + it('should support alias', () => { + expect( + mock(/*js*/ ` + Component(({'foo': renamed, bar: anotherName}) => {}) + `) + ).toMatchInlineSnapshot(` + "Component(({ + foo, + bar + }) => { + let foo_$p$_ = foo; + let renamed = foo_$p$_; + let bar_$p$_ = bar; + let anotherName = bar_$p$_; + });" + `); + }); + + it('should support nested props', () => { + expect( + mock(/*js*/ ` + Component(({foo: {nested1, nested2}, bar}) => {}) + `) + ).toMatchInlineSnapshot(` + "Component(({ + foo, + bar + }) => { + let foo_$p$_ = foo; + let { + nested1, + nested2 + } = foo_$p$_; + let bar_$p$_ = bar; + });" + `); + }); + // + 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 ({ + prop1, + prop2 + }) { + let prop1_$p$_ = prop1; + let prop2_$p$_ = prop2; + let { + p2: [p20X = defaultVal, { + p211, + p212: p212X = defaultVal + }, ...restArr], + p3, + ...restObj + } = prop2_$p$_; + });" + `); + }); + + it('should support rest element', () => { + expect( + mock(/*js*/ ` + Component(({foo, ...rest}) => {}) + `) + ).toMatchInlineSnapshot(` + "Component(({ + foo, + ...rest + }) => { + let foo_$p$_ = foo; + let rest_$p$_ = rest; + });" + `); + }); + + 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(({ + foo, + bar + }) => { + let foo_$p$_ = foo; + let bar_$p$_ = bar; + });" + `); + }); + + it('should support props renaming', () => { + expect( + mock(/*js*/ ` + Component((props) => { + const newProps = props; + const {foo: renamed, bar} = newProps; + }) + `) + ).toMatchInlineSnapshot(` + "Component(({ + foo, + bar + }) => { + let foo_$p$_ = foo; + let renamed = foo_$p$_; + let bar_$p$_ = bar; + });" + `); + }); + }); +}); diff --git a/packages/transpiler/babel-inula-next-core/tsconfig.json b/packages/transpiler/babel-inula-next-core/tsconfig.json index 0cdc98df60aa519d3da270244e76e909caf925c3..32529a45eca5c84c1dbd132c2caed4610454bb29 100644 --- a/packages/transpiler/babel-inula-next-core/tsconfig.json +++ b/packages/transpiler/babel-inula-next-core/tsconfig.json @@ -1,20 +1,20 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "lib": [ - "ESNext", - "DOM" - ], - "moduleResolution": "Node", - "strict": true, - "esModuleInterop": true - }, - "ts-node": { - "esm": true - }, - "exclude": [ - "node_modules", - "dist" - ] -} +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": [ + "ESNext", + "DOM" + ], + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true + }, + "ts-node": { + "esm": true + }, + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/packages/transpiler/error-handler/src/index.ts b/packages/transpiler/error-handler/src/index.ts index 83ef579f6c1bbcce63877d357af0e4f54267a3b5..04db8e6d1da6a69a6ef7da27e425598330062015 100644 --- a/packages/transpiler/error-handler/src/index.ts +++ b/packages/transpiler/error-handler/src/index.ts @@ -1,62 +1,62 @@ -type InulaNextErrMap = Record; -type ErrorMethod = { - [K in keyof T as `${G}${K & number}`]: (...args: string[]) => any; -}; - -/** - * @brief Create error handler by given error space and error maps - * e.g. - * const errHandler = createErrorHandler("InulaNext", { - * 1: "Cannot find node type: $0, throw" - * }, { - * 1: "This is an error: $0" - * }, { - * 1: "It's a warning" - * }) - * errHandler.throw1("div") // -> throw new Error(":D - InulaNext[throw1]: Cannot find node type: div, throw") - * errHandler.error1("div") // -> console.error(":D - InulaNext[error1]: This is an error: div") - * errHandler.warn1() // -> console.warn(":D - InulaNext[warn1]: It's a warning") - * @param errorSpace - * @param throwMap - * @param errorMap - * @param warningMap - * @returns Error handler - */ -export function createErrorHandler( - errorSpace: string, - throwMap: A = {} as any, - errorMap: B = {} as any, - warningMap: C = {} as any -) { - function handleError(map: InulaNextErrMap, type: string, func: (msg: string) => any) { - return Object.fromEntries( - Object.entries(map).map(([code, msg]) => [ - `${type}${code}`, - (...args: string[]) => { - args.forEach((arg, i) => { - msg = msg.replace(`$${i}`, arg); - }); - return func(`:D - ${errorSpace}[${type}${code}]: ${msg}`); - }, - ]) - ); - } - const methods: ErrorMethod & ErrorMethod & ErrorMethod = { - ...handleError(throwMap, 'throw', msg => { - throw new Error(msg); - }), - ...handleError(errorMap, 'error', console.error), - ...handleError(warningMap, 'warn', console.warn), - } as any; - - function notDescribed(type: string) { - return () => `:D ${errorSpace}: ${type} not described`; - } - - return { - ...methods, - throwUnknown: notDescribed('throw'), - errorUnknown: notDescribed('error'), - warnUnknown: notDescribed('warn'), - }; -} +type InulaNextErrMap = Record; +type ErrorMethod = { + [K in keyof T as `${G}${K & number}`]: (...args: string[]) => any; +}; + +/** + * @brief Create error handler by given error space and error maps + * e.g. + * const errHandler = createErrorHandler("InulaNext", { + * 1: "Cannot find node type: $0, throw" + * }, { + * 1: "This is an error: $0" + * }, { + * 1: "It's a warning" + * }) + * errHandler.throw1("div") // -> throw new Error(":D - InulaNext[throw1]: Cannot find node type: div, throw") + * errHandler.error1("div") // -> console.error(":D - InulaNext[error1]: This is an error: div") + * errHandler.warn1() // -> console.warn(":D - InulaNext[warn1]: It's a warning") + * @param errorSpace + * @param throwMap + * @param errorMap + * @param warningMap + * @returns Error handler + */ +export function createErrorHandler( + errorSpace: string, + throwMap: A = {} as any, + errorMap: B = {} as any, + warningMap: C = {} as any +) { + function handleError(map: InulaNextErrMap, type: string, func: (msg: string) => any) { + return Object.fromEntries( + Object.entries(map).map(([code, msg]) => [ + `${type}${code}`, + (...args: string[]) => { + args.forEach((arg, i) => { + msg = msg.replace(`$${i}`, arg); + }); + return func(`:D - ${errorSpace}[${type}${code}]: ${msg}`); + }, + ]) + ); + } + const methods: ErrorMethod & ErrorMethod & ErrorMethod = { + ...handleError(throwMap, 'throw', msg => { + throw new Error(msg); + }), + ...handleError(errorMap, 'error', console.error), + ...handleError(warningMap, 'warn', console.warn), + } as any; + + function notDescribed(type: string) { + return () => `:D ${errorSpace}: ${type} not described`; + } + + return { + ...methods, + throwUnknown: notDescribed('throw'), + errorUnknown: notDescribed('error'), + warnUnknown: notDescribed('warn'), + }; +} diff --git a/packages/transpiler/error-handler/tsconfig.json b/packages/transpiler/error-handler/tsconfig.json index cbd364bea050f939c827659f655b319a1193ab36..65cbc2556a2da1ef9383ecf73c2d3dad81eb310a 100644 --- a/packages/transpiler/error-handler/tsconfig.json +++ b/packages/transpiler/error-handler/tsconfig.json @@ -1,12 +1,12 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", - "strict": true - }, - "ts-node": { - "esm": true - } +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true + }, + "ts-node": { + "esm": true + } } \ No newline at end of file diff --git a/packages/transpiler/jsx-parser/src/index.ts b/packages/transpiler/jsx-parser/src/index.ts index 148158542058a80ba2bca961f0cc260188e2e3e9..d7b8993966d5d38520a0ed0662786803bcca0485 100644 --- a/packages/transpiler/jsx-parser/src/index.ts +++ b/packages/transpiler/jsx-parser/src/index.ts @@ -1,14 +1,14 @@ -import { ViewParser } from './parser'; -import type { ViewUnit, ViewParserConfig, AllowedJSXNode } from './types'; - -/** - * @brief Generate view units from a babel ast - * @param node - * @param config - * @returns ViewUnit[] - */ -export function parseView(node: AllowedJSXNode, config: ViewParserConfig): ViewUnit[] { - return new ViewParser(config).parse(node); -} - -export * from './types'; +import { ViewParser } from './parser'; +import type { ViewUnit, ViewParserConfig, AllowedJSXNode } from './types'; + +/** + * @brief Generate view units from a babel ast + * @param node + * @param config + * @returns ViewUnit[] + */ +export function parseView(node: AllowedJSXNode, config: ViewParserConfig): ViewUnit[] { + return new ViewParser(config).parse(node); +} + +export * from './types'; diff --git a/packages/transpiler/jsx-parser/src/parser.ts b/packages/transpiler/jsx-parser/src/parser.ts index 829528a600c356c2fb672ef8cbbc8fc9e123c8a7..25c92149371cb88e2cdede16837edbaffeeeb3db 100644 --- a/packages/transpiler/jsx-parser/src/parser.ts +++ b/packages/transpiler/jsx-parser/src/parser.ts @@ -1,614 +1,614 @@ -import type { NodePath, types as t, traverse as tr } from '@babel/core'; -import type { - UnitProp, - ViewUnit, - ViewParserConfig, - AllowedJSXNode, - HTMLUnit, - TextUnit, - MutableUnit, - TemplateProp, - Context, -} from './types'; -import { cleanJSXText } from './utils'; - -function isContext(str: string) { - return /^[A-Z][a-zA-Z0-9]*Context/.test(str); -} - -export class ViewParser { - // ---- Namespace and tag name - private readonly htmlNamespace: string = 'html'; - private readonly htmlTagNamespace: string = 'tag'; - private readonly compTagNamespace: string = 'comp'; - private readonly envTagName: string = 'env'; - private readonly forTagName: string = 'for'; - private readonly ifTagName: string = 'if'; - private readonly elseIfTagName: string = 'else-if'; - private readonly elseTagName: string = 'else'; - private readonly customHTMLProps: string[] = ['ref']; - - private readonly config: ViewParserConfig; - private readonly htmlTags: string[]; - private readonly willParseTemplate: boolean; - - private readonly t: typeof t; - private readonly traverse: typeof tr; - - private readonly viewUnits: ViewUnit[] = []; - private context: Context; - - /** - * @brief Constructor - * @param config - * @param context - */ - constructor(config: ViewParserConfig, context: Context = { ifElseStack: [] }) { - this.config = config; - this.t = config.babelApi.types; - this.traverse = config.babelApi.traverse; - this.htmlTags = config.htmlTags; - this.willParseTemplate = config.parseTemplate ?? true; - this.context = context; - } - - /** - * @brief Parse the node into view units - * @param node - * @returns ViewUnit[] - */ - parse(node: AllowedJSXNode): ViewUnit[] { - if (this.t.isJSXText(node)) this.parseText(node); - else if (this.t.isJSXExpressionContainer(node)) this.parseExpression(node.expression); - else if (this.t.isJSXElement(node)) this.parseElement(node); - else if (this.t.isJSXFragment(node)) { - node.children.forEach(child => { - this.parse(child); - }); - } - - return this.viewUnits; - } - - /** - * @brief Parse JSXText - * @param node - */ - private parseText(node: t.JSXText): void { - const text = cleanJSXText(node); - if (!text) return; - this.viewUnits.push({ - type: 'text', - content: this.t.stringLiteral(text), - }); - } - - /** - * @brief Parse JSXExpressionContainer - * @param node - */ - private parseExpression(node: t.Expression | t.JSXEmptyExpression): void { - if (this.t.isJSXEmptyExpression(node)) return; - if (this.t.isLiteral(node) && !this.t.isTemplateLiteral(node)) { - // ---- Treat literal as text except template literal - // Cuz template literal may have viewProp inside like: - // <>{i18n`hello ${}`} - this.viewUnits.push({ - type: 'text', - content: node, - }); - return; - } - this.viewUnits.push({ - type: 'exp', - content: this.parseProp(node), - props: {}, - }); - } - - /** - * @brief Parse JSXElement - * @param node - */ - private parseElement(node: t.JSXElement): void { - let type: 'html' | 'comp'; - let tag: t.Expression; - - // ---- Parse tag and type - const openingName = node.openingElement.name; - if (this.t.isJSXIdentifier(openingName)) { - // ---- Opening name is a JSXIdentifier, e.g.,
    - const name = openingName.name; - // ---- Specially parse if and env - if ([this.ifTagName, this.elseIfTagName, this.elseTagName].includes(name)) return this.parseIf(node); - if (isContext(name)) return this.parseContext(node, name); - if (name === this.forTagName) return this.parseFor(node); - else if (this.htmlTags.includes(name)) { - type = 'html'; - tag = this.t.stringLiteral(name); - } else { - // ---- If the name is not in htmlTags, treat it as a comp - type = 'comp'; - tag = this.t.identifier(name); - } - } else if (this.t.isJSXMemberExpression(openingName)) { - // ---- Opening name is a JSXMemberExpression, e.g., - // Treat it as a comp and set the tag as the opening name - type = 'comp'; - // ---- Turn JSXMemberExpression into MemberExpression recursively - const toMemberExpression = (node: t.JSXMemberExpression): t.MemberExpression => { - if (this.t.isJSXMemberExpression(node.object)) { - return this.t.memberExpression(toMemberExpression(node.object), this.t.identifier(node.property.name)); - } - return this.t.memberExpression(this.t.identifier(node.object.name), this.t.identifier(node.property.name)); - }; - tag = toMemberExpression(openingName); - } else { - // ---- isJSXNamespacedName - const namespace = openingName.namespace.name; - switch (namespace) { - case this.compTagNamespace: - // ---- If the namespace is the same as the compTagNamespace, treat it as a comp - // and set the tag as an identifier - // e.g., => ["comp", div] - // this means you've declared a component named "div" and force it to be a comp instead an html - type = 'comp'; - tag = this.t.identifier(openingName.name.name); - break; - case this.htmlNamespace: - // ---- If the namespace is the same as the htmlTagNamespace, treat it as an html - // and set the tag as a string literal - // e.g., => ["html", "MyWebComponent"] - // the tag will be treated as a string, i.e., - type = 'html'; - tag = this.t.stringLiteral(openingName.name.name); - break; - case this.htmlTagNamespace: - // ---- If the namespace is the same as the htmlTagNamespace, treat it as an html - // and set the tag as an identifier - // e.g., => ["html", variable] - // this unit will be htmlUnit and the html string tag is stored in "variable" - type = 'html'; - tag = this.t.identifier(openingName.name.name); - break; - default: - // ---- Otherwise, treat it as a html tag and make the tag as the namespace:name - type = 'html'; - tag = this.t.stringLiteral(`${namespace}:${openingName.name.name}`); - break; - } - } - - // ---- Parse the props - const props = node.openingElement.attributes; - const propMap: Record = Object.fromEntries(props.map(prop => this.parseJSXProp(prop))); - - // ---- Parse the children - const childUnits = node.children.map(child => this.parseView(child)).flat(); - - let unit: ViewUnit = { type, tag, props: propMap, children: childUnits }; - - if (unit.type === 'html' && childUnits.length === 1 && childUnits[0].type === 'text') { - // ---- If the html unit only has one text child, merge the text into the html unit using attribute `textContent` - const text = childUnits[0] as TextUnit; - unit = { - ...unit, - children: [], - props: { - ...unit.props, - textContent: { - value: text.content, - viewPropMap: {}, - }, - }, - }; - } - - if (unit.type === 'html') unit = this.transformTemplate(unit); - - this.viewUnits.push(unit); - } - - /** - * @brief Parse ContextProvider - * @param node - * @param contextName - */ - private parseContext(node: t.JSXElement, contextName: string): void { - const props = node.openingElement.attributes; - const propMap: Record = Object.fromEntries(props.map(prop => this.parseJSXProp(prop))); - const children = node.children.map(child => this.parseView(child)).flat(); - this.viewUnits.push({ - type: 'context', - props: propMap, - contextName, - children, - }); - } - - private parseIf(node: t.JSXElement): void { - const name = (node.openingElement.name as t.JSXIdentifier).name; - // ---- else - if (name === this.elseTagName) { - const lastUnit = this.context.ifElseStack[this.context.ifElseStack.length - 1]; - if (!lastUnit || lastUnit.type !== 'if') throw new Error(`Missing if for ${name}`); - lastUnit.branches.push({ - condition: this.t.booleanLiteral(true), - children: node.children.map(child => this.parseView(child)).flat(), - }); - this.context.ifElseStack.pop(); - return; - } - - const condition = node.openingElement.attributes.filter( - attr => this.t.isJSXAttribute(attr) && attr.name.name === 'cond' - )[0]; - if (!condition) throw new Error(`Missing condition for ${name}`); - if (!this.t.isJSXAttribute(condition)) throw new Error(`JSXSpreadAttribute is not supported for ${name} condition`); - if (!this.t.isJSXExpressionContainer(condition.value) || !this.t.isExpression(condition.value.expression)) - throw new Error(`Invalid condition for ${name}`); - - // ---- if - if (name === this.ifTagName) { - const unit = { - type: 'if' as const, - branches: [ - { - condition: condition.value.expression, - children: node.children.map(child => this.parseView(child)).flat(), - }, - ], - }; - this.viewUnits.push(unit); - this.context.ifElseStack.push(unit); - return; - } - - // ---- else-if - const lastUnit = this.context.ifElseStack[this.context.ifElseStack.length - 1]; - if (!lastUnit || lastUnit.type !== 'if') throw new Error(`Missing if for ${name}`); - - lastUnit.branches.push({ - condition: condition.value.expression, - children: node.children.map(child => this.parseView(child)).flat(), - }); - } - - /** - * @brief Parse JSXAttribute or JSXSpreadAttribute into UnitProp, - * considering both namespace and expression - * @param prop - * @returns [propName, propValue] - */ - private parseJSXProp(prop: t.JSXAttribute | t.JSXSpreadAttribute): [string, UnitProp] { - if (this.t.isJSXAttribute(prop)) { - let propName: string, specifier: string | undefined; - if (this.t.isJSXNamespacedName(prop.name)) { - // ---- If the prop name is a JSXNamespacedName, e.g., bind:value - // give it a special tag - propName = prop.name.name.name; - specifier = prop.name.namespace.name; - } else { - propName = prop.name.name; - } - let value = this.t.isJSXExpressionContainer(prop.value) ? prop.value.expression : prop.value; - if (this.t.isJSXEmptyExpression(value)) value = undefined; - return [propName, this.parseProp(value, specifier)]; - } - // ---- Use *spread* as the propName to avoid conflict with other props - return ['*spread*', this.parseProp(prop.argument)]; - } - - /** - * @brief Parse the prop node into UnitProp - * @param propNode - * @param specifier - * @returns UnitProp - */ - private parseProp(propNode: t.Expression | undefined | null, specifier?: string): UnitProp { - // ---- If there is no propNode, set the default prop as true - if (!propNode) { - return { - value: this.t.booleanLiteral(true), - viewPropMap: {}, - }; - } - - // ---- Collect sub jsx nodes as Prop - const viewPropMap: Record = {}; - const parseViewProp = (innerPath: NodePath): void => { - const id = this.uid(); - const node = innerPath.node; - viewPropMap[id] = this.parseView(node); - const newNode = this.t.stringLiteral(id); - if (node === propNode) { - // ---- If the node is the propNode, replace it with the new node - propNode = newNode; - } - // ---- Replace the node and skip the inner path - innerPath.replaceWith(newNode); - innerPath.skip(); - }; - - // ---- Apply the parseViewProp to JSXElement and JSXFragment - this.traverse(this.wrapWithFile(propNode), { - JSXElement: parseViewProp, - JSXFragment: parseViewProp, - }); - - return { - value: propNode, - viewPropMap, - specifier, - }; - } - - transformTemplate(unit: ViewUnit): ViewUnit { - if (!this.willParseTemplate) return unit; - if (!this.isHTMLTemplate(unit)) return unit; - unit = unit as HTMLUnit; - return { - type: 'template', - template: this.generateTemplate(unit), - mutableUnits: this.generateMutableUnits(unit), - props: this.parseTemplateProps(unit), - }; - } - - /** - * @brief Generate the entire HTMLUnit - * @param unit - * @returns HTMLUnit - */ - private generateTemplate(unit: HTMLUnit): HTMLUnit { - const staticProps = Object.fromEntries( - this.filterTemplateProps( - // ---- Get all the static props - Object.entries(unit.props ?? []).filter( - ([, prop]) => - this.isStaticProp(prop) && - // ---- Filter out props with false values - !(this.t.isBooleanLiteral(prop.value) && !prop.value.value) - ) - ) - ); - - let children: (HTMLUnit | TextUnit)[] = []; - if (unit.children) { - children = unit.children - .map(unit => { - if (unit.type === 'text') return unit; - if (unit.type === 'html' && this.t.isStringLiteral(unit.tag)) { - return this.generateTemplate(unit); - } - }) - .filter(Boolean) as (HTMLUnit | TextUnit)[]; - } - return { - type: 'html', - tag: unit.tag, - props: staticProps, - children, - }; - } - - /** - * @brief Collect all the mutable nodes in a static HTMLUnit - * We use this function to collect mutable nodes' path and props, - * so that in the generate, we know which position to insert the mutable nodes - * @param htmlUnit - * @returns mutable particles - */ - private generateMutableUnits(htmlUnit: HTMLUnit): MutableUnit[] { - const mutableUnits: MutableUnit[] = []; - - const generateMutableUnit = (unit: HTMLUnit, path: number[] = []) => { - const maxHtmlIdx = unit.children?.filter( - child => (child.type === 'html' && this.t.isStringLiteral(child.tag)) || child.type === 'text' - ).length; - let htmlIdx = -1; - // ---- Generate mutable unit for current HTMLUnit - unit.children?.forEach(child => { - if (!(child.type === 'html' && this.t.isStringLiteral(child.tag)) && !(child.type === 'text')) { - const idx = htmlIdx + 1 >= maxHtmlIdx ? -1 : htmlIdx + 1; - mutableUnits.push({ - path: [...path, idx], - ...this.transformTemplate(child), - }); - } else { - htmlIdx++; - } - }); - // ---- Recursively generate mutable units for static HTMLUnit children - unit.children - ?.filter(child => child.type === 'html' && this.t.isStringLiteral(child.tag)) - .forEach((child, idx) => { - generateMutableUnit(child as HTMLUnit, [...path, idx]); - }); - }; - generateMutableUnit(htmlUnit); - - return mutableUnits; - } - - /** - * @brief Collect all the props in a static HTMLUnit or its nested HTMLUnit children - * Just like the mutable nodes, props are also equipped with path, - * so that we know which HTML ChildNode to insert the props - * @param htmlUnit - * @returns props - */ - private parseTemplateProps(htmlUnit: HTMLUnit): TemplateProp[] { - const templateProps: TemplateProp[] = []; - const generateVariableProp = (unit: HTMLUnit, path: number[]) => { - // ---- Generate all non-static(string/number/boolean) props for current HTMLUnit - // to be inserted further in the generate - unit.props && - Object.entries(unit.props) - .filter(([, prop]) => !this.isStaticProp(prop)) - .forEach(([key, prop]) => { - templateProps.push({ - tag: unit.tag, - name: (unit.tag as t.StringLiteral).value, - key, - path, - value: prop.value, - }); - }); - // ---- Recursively generate props for static HTMLUnit children - unit.children - ?.filter(child => child.type === 'html' && this.t.isStringLiteral(child.tag)) - .forEach((child, idx) => { - generateVariableProp(child as HTMLUnit, [...path, idx]); - }); - }; - generateVariableProp(htmlUnit, []); - - return templateProps; - } - - /** - * @brief Check if a ViewUnit is a static HTMLUnit that can be parsed into a template - * Must satisfy: - * 1. type is html - * 2. tag is a string literal, i.e., non-dynamic tag - * 3. has at least one child that is a static HTMLUnit, - * or else just call a createElement function, no need for template clone - * @param viewUnit - * @returns is a static HTMLUnit - */ - private isHTMLTemplate(viewUnit: ViewUnit): boolean { - return ( - viewUnit.type === 'html' && - this.t.isStringLiteral(viewUnit.tag) && - !!viewUnit.children?.some(child => child.type === 'html' && this.t.isStringLiteral(child.tag)) - ); - } - - private isStaticProp(prop: UnitProp): boolean { - return ( - this.t.isStringLiteral(prop.value) || - this.t.isNumericLiteral(prop.value) || - this.t.isBooleanLiteral(prop.value) || - this.t.isNullLiteral(prop.value) - ); - } - - /** - * @brief Filter out some props that are not needed in the template, - * these are all special props to be parsed differently in the generate - * @param props - * @returns filtered props - */ - private filterTemplateProps(props: Array<[string, T]>): Array<[string, T]> { - return ( - props - // ---- Filter out event listeners - .filter(([key]) => !key.startsWith('on')) - // ---- Filter out specific props - .filter(([key]) => !this.customHTMLProps.includes(key)) - ); - } - - /** - * @brief Parse the view by duplicating current parser's classRootPath, statements and htmlTags - * @param statements - * @returns ViewUnit[] - */ - private parseView(node: AllowedJSXNode): ViewUnit[] { - return new ViewParser({ ...this.config, parseTemplate: false }, this.context).parse(node); - } - - /** - * @brief Wrap the value in a file - * @param node - * @returns wrapped value - */ - private wrapWithFile(node: t.Expression): t.File { - return this.t.file(this.t.program([this.t.expressionStatement(node)])); - } - - /** - * @brief Generate a unique id - * @returns a unique id - */ - private uid(): string { - return Math.random().toString(36).slice(2); - } - - private findProp(node: t.JSXElement, name: string) { - const props = node.openingElement.attributes; - - return props.find((prop): prop is t.JSXAttribute => this.t.isJSXAttribute(prop) && prop.name.name === name); - } - - private parseFor(node: t.JSXElement) { - // ---- Get array - const arrayContainer = this.findProp(node, 'each'); - if (!arrayContainer) throw new Error('Missing [each] prop in for loop'); - if (!this.t.isJSXExpressionContainer(arrayContainer.value)) - throw new Error('Expected expression container for [array] prop'); - const array = arrayContainer.value.expression; - if (this.t.isJSXEmptyExpression(array)) throw new Error('Expected [array] expression not empty'); - - // ---- Get key - const keyProp = this.findProp(node, 'key'); - let key: t.Expression = this.t.nullLiteral(); - if (keyProp) { - if (!this.t.isJSXExpressionContainer(keyProp.value)) throw new Error('Expected expression container'); - if (this.t.isJSXEmptyExpression(keyProp.value.expression)) throw new Error('Expected expression not empty'); - key = keyProp.value.expression; - } - - // ---- Get Item - // - // {(item, idx)=>()} - // - const jsxChildren = node.children.find(child => this.t.isJSXExpressionContainer(child)) as t.JSXExpressionContainer; - if (!jsxChildren) throw new Error('Expected expression container'); - const itemFnNode = jsxChildren.expression; - if (this.t.isJSXEmptyExpression(itemFnNode)) throw new Error('Expected expression not empty'); - - let children; - if (!this.t.isFunctionExpression(itemFnNode) && !this.t.isArrowFunctionExpression(itemFnNode)) { - throw new Error('For: Expected function expression'); - } - // get the return value - if (this.t.isBlockStatement(itemFnNode.body)) { - if (itemFnNode.body.body.length !== 1) throw new Error('For: Expected 1 statement in block statement'); - if (!this.t.isReturnStatement(itemFnNode.body.body[0])) - throw new Error('For: Expected return statement in block statement'); - children = itemFnNode.body.body[0].argument; - } else { - children = itemFnNode.body; - } - - if (this.t.isJSXElement(children)) { - const keyAttr = children.openingElement.attributes.find( - (attr): attr is t.JSXAttribute => this.t.isJSXAttribute(attr) && attr.name.name === 'key' - ); - if ( - keyAttr?.value && - this.t.isJSXExpressionContainer(keyAttr.value) && - !this.t.isJSXEmptyExpression(keyAttr.value.expression) - ) { - key = keyAttr.value.expression; - } - } - - const item = itemFnNode.params[0]; - const index = itemFnNode.params[1] || null; - if (index && !this.t.isIdentifier(index)) - throw new Error('For: Expected identifier in function second parameter as index'); - if (!this.t.isJSXElement(children)) throw new Error('For: Expected jsx element in return statement'); - - this.viewUnits.push({ - type: 'for', - key, - item, - index, - array, - children: this.parseView(children), - }); - } -} +import type { NodePath, types as t, traverse as tr } from '@babel/core'; +import type { + UnitProp, + ViewUnit, + ViewParserConfig, + AllowedJSXNode, + HTMLUnit, + TextUnit, + MutableUnit, + TemplateProp, + Context, +} from './types'; +import { cleanJSXText } from './utils'; + +function isContext(str: string) { + return /^[A-Z][a-zA-Z0-9]*Context/.test(str); +} + +export class ViewParser { + // ---- Namespace and tag name + private readonly htmlNamespace: string = 'html'; + private readonly htmlTagNamespace: string = 'tag'; + private readonly compTagNamespace: string = 'comp'; + private readonly envTagName: string = 'env'; + private readonly forTagName: string = 'for'; + private readonly ifTagName: string = 'if'; + private readonly elseIfTagName: string = 'else-if'; + private readonly elseTagName: string = 'else'; + private readonly customHTMLProps: string[] = ['ref']; + + private readonly config: ViewParserConfig; + private readonly htmlTags: string[]; + private readonly willParseTemplate: boolean; + + private readonly t: typeof t; + private readonly traverse: typeof tr; + + private readonly viewUnits: ViewUnit[] = []; + private context: Context; + + /** + * @brief Constructor + * @param config + * @param context + */ + constructor(config: ViewParserConfig, context: Context = { ifElseStack: [] }) { + this.config = config; + this.t = config.babelApi.types; + this.traverse = config.babelApi.traverse; + this.htmlTags = config.htmlTags; + this.willParseTemplate = config.parseTemplate ?? true; + this.context = context; + } + + /** + * @brief Parse the node into view units + * @param node + * @returns ViewUnit[] + */ + parse(node: AllowedJSXNode): ViewUnit[] { + if (this.t.isJSXText(node)) this.parseText(node); + else if (this.t.isJSXExpressionContainer(node)) this.parseExpression(node.expression); + else if (this.t.isJSXElement(node)) this.parseElement(node); + else if (this.t.isJSXFragment(node)) { + node.children.forEach(child => { + this.parse(child); + }); + } + + return this.viewUnits; + } + + /** + * @brief Parse JSXText + * @param node + */ + private parseText(node: t.JSXText): void { + const text = cleanJSXText(node); + if (!text) return; + this.viewUnits.push({ + type: 'text', + content: this.t.stringLiteral(text), + }); + } + + /** + * @brief Parse JSXExpressionContainer + * @param node + */ + private parseExpression(node: t.Expression | t.JSXEmptyExpression): void { + if (this.t.isJSXEmptyExpression(node)) return; + if (this.t.isLiteral(node) && !this.t.isTemplateLiteral(node)) { + // ---- Treat literal as text except template literal + // Cuz template literal may have viewProp inside like: + // <>{i18n`hello ${}`} + this.viewUnits.push({ + type: 'text', + content: node, + }); + return; + } + this.viewUnits.push({ + type: 'exp', + content: this.parseProp(node), + props: {}, + }); + } + + /** + * @brief Parse JSXElement + * @param node + */ + private parseElement(node: t.JSXElement): void { + let type: 'html' | 'comp'; + let tag: t.Expression; + + // ---- Parse tag and type + const openingName = node.openingElement.name; + if (this.t.isJSXIdentifier(openingName)) { + // ---- Opening name is a JSXIdentifier, e.g.,
    + const name = openingName.name; + // ---- Specially parse if and env + if ([this.ifTagName, this.elseIfTagName, this.elseTagName].includes(name)) return this.parseIf(node); + if (isContext(name)) return this.parseContext(node, name); + if (name === this.forTagName) return this.parseFor(node); + else if (this.htmlTags.includes(name)) { + type = 'html'; + tag = this.t.stringLiteral(name); + } else { + // ---- If the name is not in htmlTags, treat it as a comp + type = 'comp'; + tag = this.t.identifier(name); + } + } else if (this.t.isJSXMemberExpression(openingName)) { + // ---- Opening name is a JSXMemberExpression, e.g., + // Treat it as a comp and set the tag as the opening name + type = 'comp'; + // ---- Turn JSXMemberExpression into MemberExpression recursively + const toMemberExpression = (node: t.JSXMemberExpression): t.MemberExpression => { + if (this.t.isJSXMemberExpression(node.object)) { + return this.t.memberExpression(toMemberExpression(node.object), this.t.identifier(node.property.name)); + } + return this.t.memberExpression(this.t.identifier(node.object.name), this.t.identifier(node.property.name)); + }; + tag = toMemberExpression(openingName); + } else { + // ---- isJSXNamespacedName + const namespace = openingName.namespace.name; + switch (namespace) { + case this.compTagNamespace: + // ---- If the namespace is the same as the compTagNamespace, treat it as a comp + // and set the tag as an identifier + // e.g., => ["comp", div] + // this means you've declared a component named "div" and force it to be a comp instead an html + type = 'comp'; + tag = this.t.identifier(openingName.name.name); + break; + case this.htmlNamespace: + // ---- If the namespace is the same as the htmlTagNamespace, treat it as an html + // and set the tag as a string literal + // e.g., => ["html", "MyWebComponent"] + // the tag will be treated as a string, i.e., + type = 'html'; + tag = this.t.stringLiteral(openingName.name.name); + break; + case this.htmlTagNamespace: + // ---- If the namespace is the same as the htmlTagNamespace, treat it as an html + // and set the tag as an identifier + // e.g., => ["html", variable] + // this unit will be htmlUnit and the html string tag is stored in "variable" + type = 'html'; + tag = this.t.identifier(openingName.name.name); + break; + default: + // ---- Otherwise, treat it as a html tag and make the tag as the namespace:name + type = 'html'; + tag = this.t.stringLiteral(`${namespace}:${openingName.name.name}`); + break; + } + } + + // ---- Parse the props + const props = node.openingElement.attributes; + const propMap: Record = Object.fromEntries(props.map(prop => this.parseJSXProp(prop))); + + // ---- Parse the children + const childUnits = node.children.map(child => this.parseView(child)).flat(); + + let unit: ViewUnit = { type, tag, props: propMap, children: childUnits }; + + if (unit.type === 'html' && childUnits.length === 1 && childUnits[0].type === 'text') { + // ---- If the html unit only has one text child, merge the text into the html unit using attribute `textContent` + const text = childUnits[0] as TextUnit; + unit = { + ...unit, + children: [], + props: { + ...unit.props, + textContent: { + value: text.content, + viewPropMap: {}, + }, + }, + }; + } + + if (unit.type === 'html') unit = this.transformTemplate(unit); + + this.viewUnits.push(unit); + } + + /** + * @brief Parse ContextProvider + * @param node + * @param contextName + */ + private parseContext(node: t.JSXElement, contextName: string): void { + const props = node.openingElement.attributes; + const propMap: Record = Object.fromEntries(props.map(prop => this.parseJSXProp(prop))); + const children = node.children.map(child => this.parseView(child)).flat(); + this.viewUnits.push({ + type: 'context', + props: propMap, + contextName, + children, + }); + } + + private parseIf(node: t.JSXElement): void { + const name = (node.openingElement.name as t.JSXIdentifier).name; + // ---- else + if (name === this.elseTagName) { + const lastUnit = this.context.ifElseStack[this.context.ifElseStack.length - 1]; + if (!lastUnit || lastUnit.type !== 'if') throw new Error(`Missing if for ${name}`); + lastUnit.branches.push({ + condition: this.t.booleanLiteral(true), + children: node.children.map(child => this.parseView(child)).flat(), + }); + this.context.ifElseStack.pop(); + return; + } + + const condition = node.openingElement.attributes.filter( + attr => this.t.isJSXAttribute(attr) && attr.name.name === 'cond' + )[0]; + if (!condition) throw new Error(`Missing condition for ${name}`); + if (!this.t.isJSXAttribute(condition)) throw new Error(`JSXSpreadAttribute is not supported for ${name} condition`); + if (!this.t.isJSXExpressionContainer(condition.value) || !this.t.isExpression(condition.value.expression)) + throw new Error(`Invalid condition for ${name}`); + + // ---- if + if (name === this.ifTagName) { + const unit = { + type: 'if' as const, + branches: [ + { + condition: condition.value.expression, + children: node.children.map(child => this.parseView(child)).flat(), + }, + ], + }; + this.viewUnits.push(unit); + this.context.ifElseStack.push(unit); + return; + } + + // ---- else-if + const lastUnit = this.context.ifElseStack[this.context.ifElseStack.length - 1]; + if (!lastUnit || lastUnit.type !== 'if') throw new Error(`Missing if for ${name}`); + + lastUnit.branches.push({ + condition: condition.value.expression, + children: node.children.map(child => this.parseView(child)).flat(), + }); + } + + /** + * @brief Parse JSXAttribute or JSXSpreadAttribute into UnitProp, + * considering both namespace and expression + * @param prop + * @returns [propName, propValue] + */ + private parseJSXProp(prop: t.JSXAttribute | t.JSXSpreadAttribute): [string, UnitProp] { + if (this.t.isJSXAttribute(prop)) { + let propName: string, specifier: string | undefined; + if (this.t.isJSXNamespacedName(prop.name)) { + // ---- If the prop name is a JSXNamespacedName, e.g., bind:value + // give it a special tag + propName = prop.name.name.name; + specifier = prop.name.namespace.name; + } else { + propName = prop.name.name; + } + let value = this.t.isJSXExpressionContainer(prop.value) ? prop.value.expression : prop.value; + if (this.t.isJSXEmptyExpression(value)) value = undefined; + return [propName, this.parseProp(value, specifier)]; + } + // ---- Use *spread* as the propName to avoid conflict with other props + return ['*spread*', this.parseProp(prop.argument)]; + } + + /** + * @brief Parse the prop node into UnitProp + * @param propNode + * @param specifier + * @returns UnitProp + */ + private parseProp(propNode: t.Expression | undefined | null, specifier?: string): UnitProp { + // ---- If there is no propNode, set the default prop as true + if (!propNode) { + return { + value: this.t.booleanLiteral(true), + viewPropMap: {}, + }; + } + + // ---- Collect sub jsx nodes as Prop + const viewPropMap: Record = {}; + const parseViewProp = (innerPath: NodePath): void => { + const id = this.uid(); + const node = innerPath.node; + viewPropMap[id] = this.parseView(node); + const newNode = this.t.stringLiteral(id); + if (node === propNode) { + // ---- If the node is the propNode, replace it with the new node + propNode = newNode; + } + // ---- Replace the node and skip the inner path + innerPath.replaceWith(newNode); + innerPath.skip(); + }; + + // ---- Apply the parseViewProp to JSXElement and JSXFragment + this.traverse(this.wrapWithFile(propNode), { + JSXElement: parseViewProp, + JSXFragment: parseViewProp, + }); + + return { + value: propNode, + viewPropMap, + specifier, + }; + } + + transformTemplate(unit: ViewUnit): ViewUnit { + if (!this.willParseTemplate) return unit; + if (!this.isHTMLTemplate(unit)) return unit; + unit = unit as HTMLUnit; + return { + type: 'template', + template: this.generateTemplate(unit), + mutableUnits: this.generateMutableUnits(unit), + props: this.parseTemplateProps(unit), + }; + } + + /** + * @brief Generate the entire HTMLUnit + * @param unit + * @returns HTMLUnit + */ + private generateTemplate(unit: HTMLUnit): HTMLUnit { + const staticProps = Object.fromEntries( + this.filterTemplateProps( + // ---- Get all the static props + Object.entries(unit.props ?? []).filter( + ([, prop]) => + this.isStaticProp(prop) && + // ---- Filter out props with false values + !(this.t.isBooleanLiteral(prop.value) && !prop.value.value) + ) + ) + ); + + let children: (HTMLUnit | TextUnit)[] = []; + if (unit.children) { + children = unit.children + .map(unit => { + if (unit.type === 'text') return unit; + if (unit.type === 'html' && this.t.isStringLiteral(unit.tag)) { + return this.generateTemplate(unit); + } + }) + .filter(Boolean) as (HTMLUnit | TextUnit)[]; + } + return { + type: 'html', + tag: unit.tag, + props: staticProps, + children, + }; + } + + /** + * @brief Collect all the mutable nodes in a static HTMLUnit + * We use this function to collect mutable nodes' path and props, + * so that in the generate, we know which position to insert the mutable nodes + * @param htmlUnit + * @returns mutable particles + */ + private generateMutableUnits(htmlUnit: HTMLUnit): MutableUnit[] { + const mutableUnits: MutableUnit[] = []; + + const generateMutableUnit = (unit: HTMLUnit, path: number[] = []) => { + const maxHtmlIdx = unit.children?.filter( + child => (child.type === 'html' && this.t.isStringLiteral(child.tag)) || child.type === 'text' + ).length; + let htmlIdx = -1; + // ---- Generate mutable unit for current HTMLUnit + unit.children?.forEach(child => { + if (!(child.type === 'html' && this.t.isStringLiteral(child.tag)) && !(child.type === 'text')) { + const idx = htmlIdx + 1 >= maxHtmlIdx ? -1 : htmlIdx + 1; + mutableUnits.push({ + path: [...path, idx], + ...this.transformTemplate(child), + }); + } else { + htmlIdx++; + } + }); + // ---- Recursively generate mutable units for static HTMLUnit children + unit.children + ?.filter(child => child.type === 'html' && this.t.isStringLiteral(child.tag)) + .forEach((child, idx) => { + generateMutableUnit(child as HTMLUnit, [...path, idx]); + }); + }; + generateMutableUnit(htmlUnit); + + return mutableUnits; + } + + /** + * @brief Collect all the props in a static HTMLUnit or its nested HTMLUnit children + * Just like the mutable nodes, props are also equipped with path, + * so that we know which HTML ChildNode to insert the props + * @param htmlUnit + * @returns props + */ + private parseTemplateProps(htmlUnit: HTMLUnit): TemplateProp[] { + const templateProps: TemplateProp[] = []; + const generateVariableProp = (unit: HTMLUnit, path: number[]) => { + // ---- Generate all non-static(string/number/boolean) props for current HTMLUnit + // to be inserted further in the generate + unit.props && + Object.entries(unit.props) + .filter(([, prop]) => !this.isStaticProp(prop)) + .forEach(([key, prop]) => { + templateProps.push({ + tag: unit.tag, + name: (unit.tag as t.StringLiteral).value, + key, + path, + value: prop.value, + }); + }); + // ---- Recursively generate props for static HTMLUnit children + unit.children + ?.filter(child => child.type === 'html' && this.t.isStringLiteral(child.tag)) + .forEach((child, idx) => { + generateVariableProp(child as HTMLUnit, [...path, idx]); + }); + }; + generateVariableProp(htmlUnit, []); + + return templateProps; + } + + /** + * @brief Check if a ViewUnit is a static HTMLUnit that can be parsed into a template + * Must satisfy: + * 1. type is html + * 2. tag is a string literal, i.e., non-dynamic tag + * 3. has at least one child that is a static HTMLUnit, + * or else just call a createElement function, no need for template clone + * @param viewUnit + * @returns is a static HTMLUnit + */ + private isHTMLTemplate(viewUnit: ViewUnit): boolean { + return ( + viewUnit.type === 'html' && + this.t.isStringLiteral(viewUnit.tag) && + !!viewUnit.children?.some(child => child.type === 'html' && this.t.isStringLiteral(child.tag)) + ); + } + + private isStaticProp(prop: UnitProp): boolean { + return ( + this.t.isStringLiteral(prop.value) || + this.t.isNumericLiteral(prop.value) || + this.t.isBooleanLiteral(prop.value) || + this.t.isNullLiteral(prop.value) + ); + } + + /** + * @brief Filter out some props that are not needed in the template, + * these are all special props to be parsed differently in the generate + * @param props + * @returns filtered props + */ + private filterTemplateProps(props: Array<[string, T]>): Array<[string, T]> { + return ( + props + // ---- Filter out event listeners + .filter(([key]) => !key.startsWith('on')) + // ---- Filter out specific props + .filter(([key]) => !this.customHTMLProps.includes(key)) + ); + } + + /** + * @brief Parse the view by duplicating current parser's classRootPath, statements and htmlTags + * @param statements + * @returns ViewUnit[] + */ + private parseView(node: AllowedJSXNode): ViewUnit[] { + return new ViewParser({ ...this.config, parseTemplate: false }, this.context).parse(node); + } + + /** + * @brief Wrap the value in a file + * @param node + * @returns wrapped value + */ + private wrapWithFile(node: t.Expression): t.File { + return this.t.file(this.t.program([this.t.expressionStatement(node)])); + } + + /** + * @brief Generate a unique id + * @returns a unique id + */ + private uid(): string { + return Math.random().toString(36).slice(2); + } + + private findProp(node: t.JSXElement, name: string) { + const props = node.openingElement.attributes; + + return props.find((prop): prop is t.JSXAttribute => this.t.isJSXAttribute(prop) && prop.name.name === name); + } + + private parseFor(node: t.JSXElement) { + // ---- Get array + const arrayContainer = this.findProp(node, 'each'); + if (!arrayContainer) throw new Error('Missing [each] prop in for loop'); + if (!this.t.isJSXExpressionContainer(arrayContainer.value)) + throw new Error('Expected expression container for [array] prop'); + const array = arrayContainer.value.expression; + if (this.t.isJSXEmptyExpression(array)) throw new Error('Expected [array] expression not empty'); + + // ---- Get key + const keyProp = this.findProp(node, 'key'); + let key: t.Expression = this.t.nullLiteral(); + if (keyProp) { + if (!this.t.isJSXExpressionContainer(keyProp.value)) throw new Error('Expected expression container'); + if (this.t.isJSXEmptyExpression(keyProp.value.expression)) throw new Error('Expected expression not empty'); + key = keyProp.value.expression; + } + + // ---- Get Item + // + // {(item, idx)=>()} + // + const jsxChildren = node.children.find(child => this.t.isJSXExpressionContainer(child)) as t.JSXExpressionContainer; + if (!jsxChildren) throw new Error('Expected expression container'); + const itemFnNode = jsxChildren.expression; + if (this.t.isJSXEmptyExpression(itemFnNode)) throw new Error('Expected expression not empty'); + + let children; + if (!this.t.isFunctionExpression(itemFnNode) && !this.t.isArrowFunctionExpression(itemFnNode)) { + throw new Error('For: Expected function expression'); + } + // get the return value + if (this.t.isBlockStatement(itemFnNode.body)) { + if (itemFnNode.body.body.length !== 1) throw new Error('For: Expected 1 statement in block statement'); + if (!this.t.isReturnStatement(itemFnNode.body.body[0])) + throw new Error('For: Expected return statement in block statement'); + children = itemFnNode.body.body[0].argument; + } else { + children = itemFnNode.body; + } + + if (this.t.isJSXElement(children)) { + const keyAttr = children.openingElement.attributes.find( + (attr): attr is t.JSXAttribute => this.t.isJSXAttribute(attr) && attr.name.name === 'key' + ); + if ( + keyAttr?.value && + this.t.isJSXExpressionContainer(keyAttr.value) && + !this.t.isJSXEmptyExpression(keyAttr.value.expression) + ) { + key = keyAttr.value.expression; + } + } + + const item = itemFnNode.params[0]; + const index = itemFnNode.params[1] || null; + if (index && !this.t.isIdentifier(index)) + throw new Error('For: Expected identifier in function second parameter as index'); + if (!this.t.isJSXElement(children)) throw new Error('For: Expected jsx element in return statement'); + + this.viewUnits.push({ + type: 'for', + key, + item, + index, + array, + children: this.parseView(children), + }); + } +} diff --git a/packages/transpiler/jsx-parser/src/test/ElementUnit.test.ts b/packages/transpiler/jsx-parser/src/test/ElementUnit.test.ts index 09f082dbdaa1d20b27b716cc7c46c0cec4a264cd..e14d558045458f10a2145856d540ea0ccabe0c7e 100644 --- a/packages/transpiler/jsx-parser/src/test/ElementUnit.test.ts +++ b/packages/transpiler/jsx-parser/src/test/ElementUnit.test.ts @@ -1,189 +1,189 @@ -import { describe, expect, it, afterAll, beforeAll } from 'vitest'; -import { config, parse, parseCode, parseView } from './mock'; -import { types as t } from '@babel/core'; -import type { CompUnit, HTMLUnit } from '../index'; - -describe('ElementUnit', () => { - beforeAll(() => { - // ---- Ignore template for this test - config.parseTemplate = false; - }); - - afterAll(() => { - config.parseTemplate = true; - }); - - // ---- Type - it('should identify a JSX element with tag in htmlTags as an HTMLUnit', () => { - const viewUnits = parse('
    '); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('html'); - }); - - it('should identify a JSX element with tag not in htmlTags as an CompUnit', () => { - const viewUnits = parse(''); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('comp'); - }); - - it('should identify a JSX element with namespaced "html" outside htmlTags as an HTMLUnit', () => { - const viewUnits = parse(''); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('html'); - }); - - it('should identify a JSX element with namespaced "tag" outside htmlTags as an HTMLUnit', () => { - const viewUnits = parse(''); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('html'); - }); - - it('should identify a JSX element with namespaced "comp" inside htmlTags as an HTMLUnit', () => { - const viewUnits = parse(''); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('comp'); - }); - - it('should identify a JSX element with name equal to "env" as an EnvUnit', () => { - const viewUnits = parse(''); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('env'); - }); - - // ---- Tag - it('should correctly parse the tag of an HTMLUnit', () => { - const viewUnits = parse('
    '); - const tag = (viewUnits[0] as HTMLUnit).tag; - - expect(t.isStringLiteral(tag, { value: 'div' })).toBeTruthy(); - }); - - it('should correctly parse the tag of an HTMLUnit with namespaced "html"', () => { - const viewUnits = parse(''); - const tag = (viewUnits[0] as HTMLUnit).tag; - - expect(t.isStringLiteral(tag, { value: 'MyWebComponent' })).toBeTruthy(); - }); - - it('should correctly parse the tag of an HTMLUnit with namespaced "tag"', () => { - const viewUnits = parse(''); - const tag = (viewUnits[0] as HTMLUnit).tag; - - expect(t.isIdentifier(tag, { name: 'variable' })).toBeTruthy(); - }); - - it('should correctly parse the tag of an CompUnit', () => { - const viewUnits = parse(''); - const tag = (viewUnits[0] as HTMLUnit).tag; - - expect(t.isIdentifier(tag, { name: 'Comp' })).toBeTruthy(); - }); - - it('should correctly parse the tag of an CompUnit with namespaced "comp"', () => { - const viewUnits = parse(''); - const tag = (viewUnits[0] as HTMLUnit).tag; - - expect(t.isIdentifier(tag, { name: 'div' })).toBeTruthy(); - }); - - // ---- Props(for both HTMLUnit and CompUnit) - it('should correctly parse the props', () => { - const viewUnits = parse('
    '); - - const htmlUnit = viewUnits[0] as HTMLUnit; - const props = htmlUnit.props!; - expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy(); - }); - - it('should correctly parse the props with a complex expression', () => { - const ast = parseCode('
    {console.log("ok")}}>
    '); - const viewUnits = parseView(ast); - - const originalExpression = ( - ((ast as t.JSXElement).openingElement.attributes[0] as t.JSXAttribute).value as t.JSXExpressionContainer - ).expression; - - const htmlUnit = viewUnits[0] as HTMLUnit; - expect(htmlUnit.props!.onClick.value).toBe(originalExpression); - }); - - it('should correctly parse multiple props', () => { - const viewUnits = parse('
    '); - - const htmlUnit = viewUnits[0] as HTMLUnit; - const props = htmlUnit.props!; - expect(Object.keys(props).length).toBe(2); - expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy(); - expect(t.isStringLiteral(props.class.value, { value: 'myClass' })).toBeTruthy(); - }); - - it('should correctly parse props with namespace as its specifier', () => { - const viewUnits = parse('
    '); - const htmlUnit = viewUnits[0] as HTMLUnit; - const props = htmlUnit.props!; - expect(props.id.specifier).toBe('bind'); - expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy(); - }); - - it('should correctly parse spread props', () => { - const viewUnits = parse(''); - const htmlUnit = viewUnits[0] as CompUnit; - const props = htmlUnit.props!; - expect(t.isIdentifier(props['*spread*'].value, { name: 'props' })).toBeTruthy(); - }); - - // ---- View prop (other test cases can be found in ExpUnit.test.ts) - it('should correctly parse sub jsx attribute as view prop', () => { - const ast = parseCode('Ok
    >'); - const viewUnits = parseView(ast); - - const props = (viewUnits[0] as CompUnit).props!; - const viewPropMap = props.sub.viewPropMap!; - expect(Object.keys(viewPropMap).length).toBe(1); - - const key = Object.keys(viewPropMap)[0]; - const viewProp = viewPropMap[key]; - expect(viewProp.length).toBe(1); - expect(viewProp[0].type).toBe('html'); - - // ---- Prop View will be replaced with a random string and stored in props.viewPropMap - const value = props.sub.value; - expect(t.isStringLiteral(value, { value: key })).toBeTruthy(); - }); - - // ---- Children(for both HTMLUnit and CompUnit) - it('should correctly parse the count of children', () => { - const viewUnits = parse(`
    -
    ok
    -
    ok
    - - -
    `); - const htmlUnit = viewUnits[0] as HTMLUnit; - expect(htmlUnit.children!.length).toBe(4); - }); - - it('should correctly parse the count of children with JSXExpressionContainer', () => { - const viewUnits = parse(`
    -
    ok
    -
    ok
    - {count} - {count} -
    `); - const htmlUnit = viewUnits[0] as HTMLUnit; - expect(htmlUnit.children!.length).toBe(4); - }); - - it('should correctly parse the count of children with JSXFragment', () => { - const viewUnits = parse(`
    -
    ok
    -
    ok
    - <> - - - -
    `); - const htmlUnit = viewUnits[0] as HTMLUnit; - expect(htmlUnit.children!.length).toBe(4); - }); -}); +import { describe, expect, it, afterAll, beforeAll } from 'vitest'; +import { config, parse, parseCode, parseView } from './mock'; +import { types as t } from '@babel/core'; +import type { CompUnit, HTMLUnit } from '../index'; + +describe('ElementUnit', () => { + beforeAll(() => { + // ---- Ignore template for this test + config.parseTemplate = false; + }); + + afterAll(() => { + config.parseTemplate = true; + }); + + // ---- Type + it('should identify a JSX element with tag in htmlTags as an HTMLUnit', () => { + const viewUnits = parse('
    '); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('html'); + }); + + it('should identify a JSX element with tag not in htmlTags as an CompUnit', () => { + const viewUnits = parse(''); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('comp'); + }); + + it('should identify a JSX element with namespaced "html" outside htmlTags as an HTMLUnit', () => { + const viewUnits = parse(''); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('html'); + }); + + it('should identify a JSX element with namespaced "tag" outside htmlTags as an HTMLUnit', () => { + const viewUnits = parse(''); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('html'); + }); + + it('should identify a JSX element with namespaced "comp" inside htmlTags as an HTMLUnit', () => { + const viewUnits = parse(''); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('comp'); + }); + + it('should identify a JSX element with name equal to "env" as an EnvUnit', () => { + const viewUnits = parse(''); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('env'); + }); + + // ---- Tag + it('should correctly parse the tag of an HTMLUnit', () => { + const viewUnits = parse('
    '); + const tag = (viewUnits[0] as HTMLUnit).tag; + + expect(t.isStringLiteral(tag, { value: 'div' })).toBeTruthy(); + }); + + it('should correctly parse the tag of an HTMLUnit with namespaced "html"', () => { + const viewUnits = parse(''); + const tag = (viewUnits[0] as HTMLUnit).tag; + + expect(t.isStringLiteral(tag, { value: 'MyWebComponent' })).toBeTruthy(); + }); + + it('should correctly parse the tag of an HTMLUnit with namespaced "tag"', () => { + const viewUnits = parse(''); + const tag = (viewUnits[0] as HTMLUnit).tag; + + expect(t.isIdentifier(tag, { name: 'variable' })).toBeTruthy(); + }); + + it('should correctly parse the tag of an CompUnit', () => { + const viewUnits = parse(''); + const tag = (viewUnits[0] as HTMLUnit).tag; + + expect(t.isIdentifier(tag, { name: 'Comp' })).toBeTruthy(); + }); + + it('should correctly parse the tag of an CompUnit with namespaced "comp"', () => { + const viewUnits = parse(''); + const tag = (viewUnits[0] as HTMLUnit).tag; + + expect(t.isIdentifier(tag, { name: 'div' })).toBeTruthy(); + }); + + // ---- Props(for both HTMLUnit and CompUnit) + it('should correctly parse the props', () => { + const viewUnits = parse('
    '); + + const htmlUnit = viewUnits[0] as HTMLUnit; + const props = htmlUnit.props!; + expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy(); + }); + + it('should correctly parse the props with a complex expression', () => { + const ast = parseCode('
    {console.log("ok")}}>
    '); + const viewUnits = parseView(ast); + + const originalExpression = ( + ((ast as t.JSXElement).openingElement.attributes[0] as t.JSXAttribute).value as t.JSXExpressionContainer + ).expression; + + const htmlUnit = viewUnits[0] as HTMLUnit; + expect(htmlUnit.props!.onClick.value).toBe(originalExpression); + }); + + it('should correctly parse multiple props', () => { + const viewUnits = parse('
    '); + + const htmlUnit = viewUnits[0] as HTMLUnit; + const props = htmlUnit.props!; + expect(Object.keys(props).length).toBe(2); + expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy(); + expect(t.isStringLiteral(props.class.value, { value: 'myClass' })).toBeTruthy(); + }); + + it('should correctly parse props with namespace as its specifier', () => { + const viewUnits = parse('
    '); + const htmlUnit = viewUnits[0] as HTMLUnit; + const props = htmlUnit.props!; + expect(props.id.specifier).toBe('bind'); + expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy(); + }); + + it('should correctly parse spread props', () => { + const viewUnits = parse(''); + const htmlUnit = viewUnits[0] as CompUnit; + const props = htmlUnit.props!; + expect(t.isIdentifier(props['*spread*'].value, { name: 'props' })).toBeTruthy(); + }); + + // ---- View prop (other test cases can be found in ExpUnit.test.ts) + it('should correctly parse sub jsx attribute as view prop', () => { + const ast = parseCode('Ok
    >'); + const viewUnits = parseView(ast); + + const props = (viewUnits[0] as CompUnit).props!; + const viewPropMap = props.sub.viewPropMap!; + expect(Object.keys(viewPropMap).length).toBe(1); + + const key = Object.keys(viewPropMap)[0]; + const viewProp = viewPropMap[key]; + expect(viewProp.length).toBe(1); + expect(viewProp[0].type).toBe('html'); + + // ---- Prop View will be replaced with a random string and stored in props.viewPropMap + const value = props.sub.value; + expect(t.isStringLiteral(value, { value: key })).toBeTruthy(); + }); + + // ---- Children(for both HTMLUnit and CompUnit) + it('should correctly parse the count of children', () => { + const viewUnits = parse(`
    +
    ok
    +
    ok
    + + +
    `); + const htmlUnit = viewUnits[0] as HTMLUnit; + expect(htmlUnit.children!.length).toBe(4); + }); + + it('should correctly parse the count of children with JSXExpressionContainer', () => { + const viewUnits = parse(`
    +
    ok
    +
    ok
    + {count} + {count} +
    `); + const htmlUnit = viewUnits[0] as HTMLUnit; + expect(htmlUnit.children!.length).toBe(4); + }); + + it('should correctly parse the count of children with JSXFragment', () => { + const viewUnits = parse(`
    +
    ok
    +
    ok
    + <> + + + +
    `); + const htmlUnit = viewUnits[0] as HTMLUnit; + expect(htmlUnit.children!.length).toBe(4); + }); +}); diff --git a/packages/transpiler/jsx-parser/src/test/ExpUnit.test.ts b/packages/transpiler/jsx-parser/src/test/ExpUnit.test.ts index 3e6f5a3702699d7d7a234b4233b10c5b15225fee..e01e5e314c21af1f371f42801e11584b052c1c0d 100644 --- a/packages/transpiler/jsx-parser/src/test/ExpUnit.test.ts +++ b/packages/transpiler/jsx-parser/src/test/ExpUnit.test.ts @@ -1,87 +1,87 @@ -import { describe, expect, it } from 'vitest'; -import { parse, parseCode, parseView, wrapWithFile } from './mock'; -import { types as t } from '@babel/core'; -import type { ExpUnit } from '../index'; -import { traverse } from '@babel/core'; - -describe('ExpUnit', () => { - // ---- Type - it('should identify expression unit', () => { - const viewUnits = parse('<>{count}'); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('exp'); - }); - - it('should not identify literals as expression unit', () => { - const viewUnits = parse('<>{1}'); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).not.toBe('exp'); - }); - - // ---- Content - it('should correctly parse content for expression unit', () => { - const viewUnits = parse('<>{count}'); - const content = (viewUnits[0] as ExpUnit).content; - - expect(t.isIdentifier(content.value, { name: 'count' })).toBeTruthy(); - }); - - it('should correctly parse complex content for expression unit', () => { - const ast = parseCode('<>{!console.log("hello world") && myComplexFunc(count + 100)}'); - const viewUnits = parseView(ast); - - const originalExpression = ((ast as t.JSXFragment).children[0] as t.JSXExpressionContainer).expression; - - const content = (viewUnits[0] as ExpUnit).content; - expect(content.value).toBe(originalExpression); - }); - - it('should correctly parse content with view prop for expression unit', () => { - // ----
    Ok
    will be replaced with a random string and stored in props.viewPropMap - const viewUnits = parse('<>{
    Ok
    }'); - const content = (viewUnits[0] as ExpUnit).content; - const viewPropMap = content.viewPropMap; - - expect(Object.keys(viewPropMap).length).toBe(1); - const key = Object.keys(viewPropMap)[0]; - const viewProp = viewPropMap[key]; - // ---- Only one view unit for
    Ok
    - expect(viewProp.length).toBe(1); - expect(viewProp[0].type).toBe('html'); - - // ---- The value of the replaced prop should be the key of the viewPropMap - const value = content.value; - expect(t.isStringLiteral(value, { value: key })).toBeTruthy(); - }); - - it('should correctly parse content with view prop for expression unit with complex expression', () => { - // ----
    Ok
    will be replaced with a random string and stored in props.viewPropMap - const ast = parseCode(`<>{ - someFunc(() => { - console.log("hello world") - doWhatever() - return
    Ok
    - }) - }`); - const viewUnits = parseView(ast); - - const content = (viewUnits[0] as ExpUnit).content; - const viewPropMap = content.viewPropMap; - - expect(Object.keys(viewPropMap).length).toBe(1); - const key = Object.keys(viewPropMap)[0]; - const viewProp = viewPropMap[key]; - // ---- Only one view unit for
    Ok
    - expect(viewProp.length).toBe(1); - - // ---- Check the value of the replaced prop - let idExistCount = 0; - traverse(wrapWithFile(content.value), { - StringLiteral(path) { - if (path.node.value === key) idExistCount++; - }, - }); - // ---- Expect the count of the id matching to be exactly 1 - expect(idExistCount).toBe(1); - }); -}); +import { describe, expect, it } from 'vitest'; +import { parse, parseCode, parseView, wrapWithFile } from './mock'; +import { types as t } from '@babel/core'; +import type { ExpUnit } from '../index'; +import { traverse } from '@babel/core'; + +describe('ExpUnit', () => { + // ---- Type + it('should identify expression unit', () => { + const viewUnits = parse('<>{count}'); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('exp'); + }); + + it('should not identify literals as expression unit', () => { + const viewUnits = parse('<>{1}'); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).not.toBe('exp'); + }); + + // ---- Content + it('should correctly parse content for expression unit', () => { + const viewUnits = parse('<>{count}'); + const content = (viewUnits[0] as ExpUnit).content; + + expect(t.isIdentifier(content.value, { name: 'count' })).toBeTruthy(); + }); + + it('should correctly parse complex content for expression unit', () => { + const ast = parseCode('<>{!console.log("hello world") && myComplexFunc(count + 100)}'); + const viewUnits = parseView(ast); + + const originalExpression = ((ast as t.JSXFragment).children[0] as t.JSXExpressionContainer).expression; + + const content = (viewUnits[0] as ExpUnit).content; + expect(content.value).toBe(originalExpression); + }); + + it('should correctly parse content with view prop for expression unit', () => { + // ----
    Ok
    will be replaced with a random string and stored in props.viewPropMap + const viewUnits = parse('<>{
    Ok
    }'); + const content = (viewUnits[0] as ExpUnit).content; + const viewPropMap = content.viewPropMap; + + expect(Object.keys(viewPropMap).length).toBe(1); + const key = Object.keys(viewPropMap)[0]; + const viewProp = viewPropMap[key]; + // ---- Only one view unit for
    Ok
    + expect(viewProp.length).toBe(1); + expect(viewProp[0].type).toBe('html'); + + // ---- The value of the replaced prop should be the key of the viewPropMap + const value = content.value; + expect(t.isStringLiteral(value, { value: key })).toBeTruthy(); + }); + + it('should correctly parse content with view prop for expression unit with complex expression', () => { + // ----
    Ok
    will be replaced with a random string and stored in props.viewPropMap + const ast = parseCode(`<>{ + someFunc(() => { + console.log("hello world") + doWhatever() + return
    Ok
    + }) + }`); + const viewUnits = parseView(ast); + + const content = (viewUnits[0] as ExpUnit).content; + const viewPropMap = content.viewPropMap; + + expect(Object.keys(viewPropMap).length).toBe(1); + const key = Object.keys(viewPropMap)[0]; + const viewProp = viewPropMap[key]; + // ---- Only one view unit for
    Ok
    + expect(viewProp.length).toBe(1); + + // ---- Check the value of the replaced prop + let idExistCount = 0; + traverse(wrapWithFile(content.value), { + StringLiteral(path) { + if (path.node.value === key) idExistCount++; + }, + }); + // ---- Expect the count of the id matching to be exactly 1 + expect(idExistCount).toBe(1); + }); +}); diff --git a/packages/transpiler/jsx-parser/src/test/ForUnit.test.ts b/packages/transpiler/jsx-parser/src/test/ForUnit.test.ts index f0e2e8777a49542d0f92a53d8e05236240b5eb78..51c475e7bd35195d21b4dd4f22718f4250392072 100644 --- a/packages/transpiler/jsx-parser/src/test/ForUnit.test.ts +++ b/packages/transpiler/jsx-parser/src/test/ForUnit.test.ts @@ -1,10 +1,10 @@ -import { describe, expect, it } from 'vitest'; -import { parse } from './mock'; - -describe('ForUnit', () => { - it('should identify for unit', () => { - const viewUnits = parse('{([x, y, z], idx) => }'); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('for'); - }); -}); +import { describe, expect, it } from 'vitest'; +import { parse } from './mock'; + +describe('ForUnit', () => { + it('should identify for unit', () => { + const viewUnits = parse('{([x, y, z], idx) => }'); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('for'); + }); +}); diff --git a/packages/transpiler/jsx-parser/src/test/IfUnit.test.ts b/packages/transpiler/jsx-parser/src/test/IfUnit.test.ts index 0c2e85f072a98e4b67f29e806305d53d04cbfc32..897df7ef8973d754df87bbb0ccff0d00a647dd20 100644 --- a/packages/transpiler/jsx-parser/src/test/IfUnit.test.ts +++ b/packages/transpiler/jsx-parser/src/test/IfUnit.test.ts @@ -1,201 +1,201 @@ -import { describe, expect, it } from 'vitest'; -import { parse } from './mock'; -import { types as t } from '@babel/core'; -import { HTMLUnit, IfUnit } from '../types'; - -describe('IfUnit', () => { - // ---- Type - it('should identify if unit', () => { - const viewUnits = parse('true'); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('if'); - }); - - it('should identify if unit with else', () => { - const viewUnits = parse(`<> - true - false - `); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('if'); - }); - - it('should identify if unit with else-if', () => { - const viewUnits = parse(`<> - true - false - `); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('if'); - }); - - it('should find matched if in html tag', () => { - const viewUnits = parse(`
    - true - false -
    `) as unknown as HTMLUnit[]; - expect(viewUnits[0].children[0].type).toBe('if'); - }); - - it('should identify if unit with else-if and else', () => { - const viewUnits = parse(`<> - true - false - else - `); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('if'); - }); - - it('should identify if unit with multiple else-if', () => { - const viewUnits = parse(`<> - true - flag1 - flag2 - else - `); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('if'); - }); - - // ---- Branches - it('should correctly parse branches count for if unit', () => { - const viewUnits = parse(`<> - true - flag1 - flag2 - else - `); - - const branches = (viewUnits[0] as IfUnit).branches; - expect(branches.length).toBe(4); - }); - - it("should correctly parse branches' condition for if unit", () => { - const viewUnits = parse('true'); - const branches = (viewUnits[0] as IfUnit).branches; - - expect(t.isBooleanLiteral(branches[0].condition, { value: true })).toBeTruthy(); - }); - - it("should correctly parse branches' children for if unit", () => { - const viewUnits = parse('true'); - const branches = (viewUnits[0] as IfUnit).branches; - - expect(branches[0].children.length).toBe(1); - expect(branches[0].children[0].type).toBe('text'); - }); - - it("should correctly parse branches' condition for if unit with else", () => { - const viewUnits = parse(`<> - 1 - 2 - `); - const branches = (viewUnits[0] as IfUnit).branches; - - expect(t.isIdentifier(branches[0].condition, { name: 'flag1' })).toBeTruthy(); - expect(t.isBooleanLiteral(branches[1].condition, { value: true })).toBeTruthy(); - }); - - it("should correctly parse branches' children for if unit with else", () => { - const viewUnits = parse(`<> - true - false - `); - const branches = (viewUnits[0] as IfUnit).branches; - - expect(branches[0].children.length).toBe(1); - expect(branches[0].children[0].type).toBe('text'); - expect(branches[1].children.length).toBe(1); - expect(branches[1].children[0].type).toBe('text'); - }); - - it("should correctly parse branches' condition for if unit with else-if", () => { - const viewUnits = parse(`<> - 1 - 2 - `); - const branches = (viewUnits[0] as IfUnit).branches; - /** - * () => { - * if (flag1) { - * this._prevCond - * return 1 - * } else () { - * if (flag2) { - * } - * } - */ - - expect(t.isIdentifier(branches[0].condition, { name: 'flag1' })).toBeTruthy(); - expect(t.isIdentifier(branches[1].condition, { name: 'flag2' })).toBeTruthy(); - }); - - it("should correctly parse branches' children for if unit with else-if", () => { - const viewUnits = parse(`<> - true - false - `); - const branches = (viewUnits[0] as IfUnit).branches; - - expect(branches[0].children.length).toBe(1); - expect(branches[0].children[0].type).toBe('text'); - expect(branches[1].children.length).toBe(1); - expect(branches[1].children[0].type).toBe('text'); - }); - - // --- nested - it('should correctly parse nested if unit', () => { - const viewUnits = parse(`<> - - true - - `); - const branches = (viewUnits[0] as IfUnit).branches; - - expect(branches.length).toBe(1); - expect(branches[0].children[0].type).toBe('if'); - }); - - it('should correctly parse nested if unit with else', () => { - const viewUnits = parse(`<> - - true - false - - `); - const branches = (viewUnits[0] as IfUnit).branches; - - expect(branches.length).toBe(1); - expect(branches[0].children[0].type).toBe('if'); - }); - - it('should throw error for nested if unit with else-if', () => { - expect(() => { - parse(`<> - - false - - `); - }).toThrowError(); - }); - - it('test', () => { - expect( - parse(` - <> -

    Hello inulax next fn comp

    -
    - count: {count}, double is: {db} - -
    - -

    Condition

    - 1}>{count} is bigger than is 1 - {count} is smaller than 1 - - - `) - ); - }); -}); +import { describe, expect, it } from 'vitest'; +import { parse } from './mock'; +import { types as t } from '@babel/core'; +import { HTMLUnit, IfUnit } from '../types'; + +describe('IfUnit', () => { + // ---- Type + it('should identify if unit', () => { + const viewUnits = parse('true'); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('if'); + }); + + it('should identify if unit with else', () => { + const viewUnits = parse(`<> + true + false + `); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('if'); + }); + + it('should identify if unit with else-if', () => { + const viewUnits = parse(`<> + true + false + `); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('if'); + }); + + it('should find matched if in html tag', () => { + const viewUnits = parse(`
    + true + false +
    `) as unknown as HTMLUnit[]; + expect(viewUnits[0].children[0].type).toBe('if'); + }); + + it('should identify if unit with else-if and else', () => { + const viewUnits = parse(`<> + true + false + else + `); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('if'); + }); + + it('should identify if unit with multiple else-if', () => { + const viewUnits = parse(`<> + true + flag1 + flag2 + else + `); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('if'); + }); + + // ---- Branches + it('should correctly parse branches count for if unit', () => { + const viewUnits = parse(`<> + true + flag1 + flag2 + else + `); + + const branches = (viewUnits[0] as IfUnit).branches; + expect(branches.length).toBe(4); + }); + + it("should correctly parse branches' condition for if unit", () => { + const viewUnits = parse('true'); + const branches = (viewUnits[0] as IfUnit).branches; + + expect(t.isBooleanLiteral(branches[0].condition, { value: true })).toBeTruthy(); + }); + + it("should correctly parse branches' children for if unit", () => { + const viewUnits = parse('true'); + const branches = (viewUnits[0] as IfUnit).branches; + + expect(branches[0].children.length).toBe(1); + expect(branches[0].children[0].type).toBe('text'); + }); + + it("should correctly parse branches' condition for if unit with else", () => { + const viewUnits = parse(`<> + 1 + 2 + `); + const branches = (viewUnits[0] as IfUnit).branches; + + expect(t.isIdentifier(branches[0].condition, { name: 'flag1' })).toBeTruthy(); + expect(t.isBooleanLiteral(branches[1].condition, { value: true })).toBeTruthy(); + }); + + it("should correctly parse branches' children for if unit with else", () => { + const viewUnits = parse(`<> + true + false + `); + const branches = (viewUnits[0] as IfUnit).branches; + + expect(branches[0].children.length).toBe(1); + expect(branches[0].children[0].type).toBe('text'); + expect(branches[1].children.length).toBe(1); + expect(branches[1].children[0].type).toBe('text'); + }); + + it("should correctly parse branches' condition for if unit with else-if", () => { + const viewUnits = parse(`<> + 1 + 2 + `); + const branches = (viewUnits[0] as IfUnit).branches; + /** + * () => { + * if (flag1) { + * this._prevCond + * return 1 + * } else () { + * if (flag2) { + * } + * } + */ + + expect(t.isIdentifier(branches[0].condition, { name: 'flag1' })).toBeTruthy(); + expect(t.isIdentifier(branches[1].condition, { name: 'flag2' })).toBeTruthy(); + }); + + it("should correctly parse branches' children for if unit with else-if", () => { + const viewUnits = parse(`<> + true + false + `); + const branches = (viewUnits[0] as IfUnit).branches; + + expect(branches[0].children.length).toBe(1); + expect(branches[0].children[0].type).toBe('text'); + expect(branches[1].children.length).toBe(1); + expect(branches[1].children[0].type).toBe('text'); + }); + + // --- nested + it('should correctly parse nested if unit', () => { + const viewUnits = parse(`<> + + true + + `); + const branches = (viewUnits[0] as IfUnit).branches; + + expect(branches.length).toBe(1); + expect(branches[0].children[0].type).toBe('if'); + }); + + it('should correctly parse nested if unit with else', () => { + const viewUnits = parse(`<> + + true + false + + `); + const branches = (viewUnits[0] as IfUnit).branches; + + expect(branches.length).toBe(1); + expect(branches[0].children[0].type).toBe('if'); + }); + + it('should throw error for nested if unit with else-if', () => { + expect(() => { + parse(`<> + + false + + `); + }).toThrowError(); + }); + + it('test', () => { + expect( + parse(` + <> +

    Hello inulax next fn comp

    +
    + count: {count}, double is: {db} + +
    + +

    Condition

    + 1}>{count} is bigger than is 1 + {count} is smaller than 1 + + + `) + ); + }); +}); diff --git a/packages/transpiler/jsx-parser/src/test/TemplateUnit.test.ts b/packages/transpiler/jsx-parser/src/test/TemplateUnit.test.ts index ca8c79f0ad9cf2f452a061923bf52732445b028b..2b9308454129ee6c7764bbd5b34f0b3ab437a221 100644 --- a/packages/transpiler/jsx-parser/src/test/TemplateUnit.test.ts +++ b/packages/transpiler/jsx-parser/src/test/TemplateUnit.test.ts @@ -1,82 +1,82 @@ -import { describe, expect, it } from 'vitest'; -import { parse } from './mock'; -import { types as t } from '@babel/core'; -import type { HTMLUnit, TemplateUnit } from '../index'; - -describe('TemplateUnit', () => { - // ---- Type - it('should not parse a single HTMLUnit to a TemplateUnit', () => { - const viewUnits = parse('
    '); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('html'); - }); - - it('should parse a nested HTMLUnit to a TemplateUnit', () => { - const viewUnits = parse('
    '); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('template'); - }); - - it('should correctly parse a nested HTMLUnit\'s structure into a template', () => { - const viewUnits = parse('
    '); - const template = (viewUnits[0] as TemplateUnit).template; - - expect(t.isStringLiteral(template.tag, { value: 'div' })).toBeTruthy(); - expect(template.children).toHaveLength(1); - const firstChild = template.children![0] as HTMLUnit; - expect(t.isStringLiteral(firstChild.tag, { value: 'div' })).toBeTruthy(); - }); - - // ---- Props - it('should correctly parse the path of TemplateUnit\'s dynamic props in root element', () => { - const viewUnits = parse('
    '); - const dynamicProps = (viewUnits[0] as TemplateUnit).props; - - expect(dynamicProps).toHaveLength(1); - const prop = dynamicProps[0]; - expect(prop.path).toHaveLength(0); - }); - - it('should correctly parse the path of TemplateUnit\'s dynamic props in nested element', () => { - const viewUnits = parse('
    '); - const dynamicProps = (viewUnits[0] as TemplateUnit).props!; - - expect(dynamicProps).toHaveLength(1); - const prop = dynamicProps[0]!; - expect(prop.path).toHaveLength(1); - expect(prop.path[0]).toBe(0); - }); - - it('should correctly parse the path of TemplateUnit\'s dynamic props with mutable particles ahead', () => { - const viewUnits = parse('
    '); - const dynamicProps = (viewUnits[0] as TemplateUnit).props!; - - expect(dynamicProps).toHaveLength(1); - const prop = dynamicProps[0]!; - expect(prop.path).toHaveLength(1); - expect(prop.path[0]).toBe(0); - }); - - it('should correctly parse the path of TemplateUnit\'s mutableUnits', () => { - const viewUnits = parse('
    '); - const mutableParticles = (viewUnits[0] as TemplateUnit).mutableUnits!; - - expect(mutableParticles).toHaveLength(1); - const particle = mutableParticles[0]!; - expect(particle.path).toHaveLength(1); - expect(particle.path[0]).toBe(0); - }); - - it('should correctly parse the path of multiple TemplateUnit\'s mutableUnits', () => { - const viewUnits = parse('
    '); - const mutableParticles = (viewUnits[0] as TemplateUnit).mutableUnits!; - - expect(mutableParticles).toHaveLength(2); - const firstParticle = mutableParticles[0]!; - expect(firstParticle.path).toHaveLength(1); - expect(firstParticle.path[0]).toBe(0); - const secondParticle = mutableParticles[1]!; - expect(secondParticle.path).toHaveLength(1); - expect(secondParticle.path[0]).toBe(-1); - }); -}); +import { describe, expect, it } from 'vitest'; +import { parse } from './mock'; +import { types as t } from '@babel/core'; +import type { HTMLUnit, TemplateUnit } from '../index'; + +describe('TemplateUnit', () => { + // ---- Type + it('should not parse a single HTMLUnit to a TemplateUnit', () => { + const viewUnits = parse('
    '); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('html'); + }); + + it('should parse a nested HTMLUnit to a TemplateUnit', () => { + const viewUnits = parse('
    '); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('template'); + }); + + it('should correctly parse a nested HTMLUnit\'s structure into a template', () => { + const viewUnits = parse('
    '); + const template = (viewUnits[0] as TemplateUnit).template; + + expect(t.isStringLiteral(template.tag, { value: 'div' })).toBeTruthy(); + expect(template.children).toHaveLength(1); + const firstChild = template.children![0] as HTMLUnit; + expect(t.isStringLiteral(firstChild.tag, { value: 'div' })).toBeTruthy(); + }); + + // ---- Props + it('should correctly parse the path of TemplateUnit\'s dynamic props in root element', () => { + const viewUnits = parse('
    '); + const dynamicProps = (viewUnits[0] as TemplateUnit).props; + + expect(dynamicProps).toHaveLength(1); + const prop = dynamicProps[0]; + expect(prop.path).toHaveLength(0); + }); + + it('should correctly parse the path of TemplateUnit\'s dynamic props in nested element', () => { + const viewUnits = parse('
    '); + const dynamicProps = (viewUnits[0] as TemplateUnit).props!; + + expect(dynamicProps).toHaveLength(1); + const prop = dynamicProps[0]!; + expect(prop.path).toHaveLength(1); + expect(prop.path[0]).toBe(0); + }); + + it('should correctly parse the path of TemplateUnit\'s dynamic props with mutable particles ahead', () => { + const viewUnits = parse('
    '); + const dynamicProps = (viewUnits[0] as TemplateUnit).props!; + + expect(dynamicProps).toHaveLength(1); + const prop = dynamicProps[0]!; + expect(prop.path).toHaveLength(1); + expect(prop.path[0]).toBe(0); + }); + + it('should correctly parse the path of TemplateUnit\'s mutableUnits', () => { + const viewUnits = parse('
    '); + const mutableParticles = (viewUnits[0] as TemplateUnit).mutableUnits!; + + expect(mutableParticles).toHaveLength(1); + const particle = mutableParticles[0]!; + expect(particle.path).toHaveLength(1); + expect(particle.path[0]).toBe(0); + }); + + it('should correctly parse the path of multiple TemplateUnit\'s mutableUnits', () => { + const viewUnits = parse('
    '); + const mutableParticles = (viewUnits[0] as TemplateUnit).mutableUnits!; + + expect(mutableParticles).toHaveLength(2); + const firstParticle = mutableParticles[0]!; + expect(firstParticle.path).toHaveLength(1); + expect(firstParticle.path[0]).toBe(0); + const secondParticle = mutableParticles[1]!; + expect(secondParticle.path).toHaveLength(1); + expect(secondParticle.path[0]).toBe(-1); + }); +}); diff --git a/packages/transpiler/jsx-parser/src/test/TextUnit.test.ts b/packages/transpiler/jsx-parser/src/test/TextUnit.test.ts index bcd435de13429c0ddfd7ae7fe987fd05e7e85d1d..2a450eef22218c136ebd879f20ccdf5992f59502 100644 --- a/packages/transpiler/jsx-parser/src/test/TextUnit.test.ts +++ b/packages/transpiler/jsx-parser/src/test/TextUnit.test.ts @@ -1,73 +1,73 @@ -import { describe, expect, it } from 'vitest'; -import { parse } from './mock'; -import { types as t } from '@babel/core'; -import type { TextUnit } from '../index'; - -describe('TextUnit', () => { - // ---- Type - it('should identify text unit', () => { - const viewUnits = parse('<>hello world'); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('text'); - }); - - it('should identify text unit with boolean expression', () => { - const viewUnits = parse('<>{true}'); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('text'); - }); - - it('should identify text unit with number expression', () => { - const viewUnits = parse('<>{1}'); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('text'); - }); - - it('should identify text unit with null expression', () => { - const viewUnits = parse('<>{null}'); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('text'); - }); - - it('should identify text unit with string literal expression', () => { - const viewUnits = parse('<>{"hello world"}'); - expect(viewUnits.length).toBe(1); - expect(viewUnits[0].type).toBe('text'); - }); - - // ---- Content - it('should correctly parse content for text unit', () => { - const viewUnits = parse('<>hello world'); - const content = (viewUnits[0] as TextUnit).content; - - expect(t.isStringLiteral(content, { value: 'hello world' })).toBeTruthy(); - }); - - it('should correctly parse content for boolean text unit', () => { - const viewUnits = parse('<>{true}'); - const content = (viewUnits[0] as TextUnit).content; - - expect(t.isBooleanLiteral(content, { value: true })).toBeTruthy(); - }); - - it('should correctly parse content for number text unit', () => { - const viewUnits = parse('<>{1}'); - const content = (viewUnits[0] as TextUnit).content; - - expect(t.isNumericLiteral(content, { value: 1 })).toBeTruthy(); - }); - - it('should correctly parse content for null text unit', () => { - const viewUnits = parse('<>{null}'); - const content = (viewUnits[0] as TextUnit).content; - - expect(t.isNullLiteral(content)).toBeTruthy(); - }); - - it('should correctly parse content for string literal text unit', () => { - const viewUnits = parse('<>{"hello world"}'); - const content = (viewUnits[0] as TextUnit).content; - - expect(t.isStringLiteral(content)).toBeTruthy(); - }); -}); +import { describe, expect, it } from 'vitest'; +import { parse } from './mock'; +import { types as t } from '@babel/core'; +import type { TextUnit } from '../index'; + +describe('TextUnit', () => { + // ---- Type + it('should identify text unit', () => { + const viewUnits = parse('<>hello world'); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('text'); + }); + + it('should identify text unit with boolean expression', () => { + const viewUnits = parse('<>{true}'); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('text'); + }); + + it('should identify text unit with number expression', () => { + const viewUnits = parse('<>{1}'); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('text'); + }); + + it('should identify text unit with null expression', () => { + const viewUnits = parse('<>{null}'); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('text'); + }); + + it('should identify text unit with string literal expression', () => { + const viewUnits = parse('<>{"hello world"}'); + expect(viewUnits.length).toBe(1); + expect(viewUnits[0].type).toBe('text'); + }); + + // ---- Content + it('should correctly parse content for text unit', () => { + const viewUnits = parse('<>hello world'); + const content = (viewUnits[0] as TextUnit).content; + + expect(t.isStringLiteral(content, { value: 'hello world' })).toBeTruthy(); + }); + + it('should correctly parse content for boolean text unit', () => { + const viewUnits = parse('<>{true}'); + const content = (viewUnits[0] as TextUnit).content; + + expect(t.isBooleanLiteral(content, { value: true })).toBeTruthy(); + }); + + it('should correctly parse content for number text unit', () => { + const viewUnits = parse('<>{1}'); + const content = (viewUnits[0] as TextUnit).content; + + expect(t.isNumericLiteral(content, { value: 1 })).toBeTruthy(); + }); + + it('should correctly parse content for null text unit', () => { + const viewUnits = parse('<>{null}'); + const content = (viewUnits[0] as TextUnit).content; + + expect(t.isNullLiteral(content)).toBeTruthy(); + }); + + it('should correctly parse content for string literal text unit', () => { + const viewUnits = parse('<>{"hello world"}'); + const content = (viewUnits[0] as TextUnit).content; + + expect(t.isStringLiteral(content)).toBeTruthy(); + }); +}); diff --git a/packages/transpiler/jsx-parser/src/test/global.d.ts b/packages/transpiler/jsx-parser/src/test/global.d.ts index a500f9b5b7b2f792db6f24f92db56e06ede2eda9..e9cc2fb268c4070982b7a690ca09b9866fd94ccf 100644 --- a/packages/transpiler/jsx-parser/src/test/global.d.ts +++ b/packages/transpiler/jsx-parser/src/test/global.d.ts @@ -1 +1 @@ -declare module '@babel/plugin-syntax-jsx'; +declare module '@babel/plugin-syntax-jsx'; diff --git a/packages/transpiler/jsx-parser/src/test/mock.ts b/packages/transpiler/jsx-parser/src/test/mock.ts index 89f8682f63a80f08a9e115a1d6ffa47539cd3445..067f47c9335be3ccd1069b69fcaf984aee985d1a 100644 --- a/packages/transpiler/jsx-parser/src/test/mock.ts +++ b/packages/transpiler/jsx-parser/src/test/mock.ts @@ -1,228 +1,228 @@ -import babel, { parseSync, types as t } from '@babel/core'; -import { AllowedJSXNode, ViewParserConfig } from '../types'; -import { parseView as pV } from '..'; -import babelJSX from '@babel/plugin-syntax-jsx'; - -const htmlTags = [ - 'a', - 'abbr', - 'address', - 'area', - 'article', - 'aside', - 'audio', - 'b', - 'base', - 'bdi', - 'bdo', - 'blockquote', - 'body', - 'br', - 'button', - 'canvas', - 'caption', - 'cite', - 'code', - 'col', - 'colgroup', - 'data', - 'datalist', - 'dd', - 'del', - 'details', - 'dfn', - 'dialog', - 'div', - 'dl', - 'dt', - 'em', - 'embed', - 'fieldset', - 'figcaption', - 'figure', - 'footer', - 'form', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'head', - 'header', - 'hgroup', - 'hr', - 'html', - 'i', - 'iframe', - 'img', - 'input', - 'ins', - 'kbd', - 'label', - 'legend', - 'li', - 'link', - 'main', - 'map', - 'mark', - 'menu', - 'meta', - 'meter', - 'nav', - 'noscript', - 'object', - 'ol', - 'optgroup', - 'option', - 'output', - 'p', - 'picture', - 'pre', - 'progress', - 'q', - 'rp', - 'rt', - 'ruby', - 's', - 'samp', - 'script', - 'section', - 'select', - 'slot', - 'small', - 'source', - 'span', - 'strong', - 'style', - 'sub', - 'summary', - 'sup', - 'table', - 'tbody', - 'td', - 'template', - 'textarea', - 'tfoot', - 'th', - 'thead', - 'time', - 'title', - 'tr', - 'track', - 'u', - 'ul', - 'var', - 'video', - 'wbr', - 'acronym', - 'applet', - 'basefont', - 'bgsound', - 'big', - 'blink', - 'center', - 'dir', - 'font', - 'frame', - 'frameset', - 'isindex', - 'keygen', - 'listing', - 'marquee', - 'menuitem', - 'multicol', - 'nextid', - 'nobr', - 'noembed', - 'noframes', - 'param', - 'plaintext', - 'rb', - 'rtc', - 'spacer', - 'strike', - 'tt', - 'xmp', - 'animate', - 'animateMotion', - 'animateTransform', - 'circle', - 'clipPath', - 'defs', - 'desc', - 'ellipse', - 'feBlend', - 'feColorMatrix', - 'feComponentTransfer', - 'feComposite', - 'feConvolveMatrix', - 'feDiffuseLighting', - 'feDisplacementMap', - 'feDistantLight', - 'feDropShadow', - 'feFlood', - 'feFuncA', - 'feFuncB', - 'feFuncG', - 'feFuncR', - 'feGaussianBlur', - 'feImage', - 'feMerge', - 'feMergeNode', - 'feMorphology', - 'feOffset', - 'fePointLight', - 'feSpecularLighting', - 'feSpotLight', - 'feTile', - 'feTurbulence', - 'filter', - 'foreignObject', - 'g', - 'image', - 'line', - 'linearGradient', - 'marker', - 'mask', - 'metadata', - 'mpath', - 'path', - 'pattern', - 'polygon', - 'polyline', - 'radialGradient', - 'rect', - 'set', - 'stop', - 'svg', - 'switch', - 'symbol', - 'text', - 'textPath', - 'tspan', - 'use', - 'view', -]; - -export const config: ViewParserConfig = { - babelApi: babel, - htmlTags, -}; - -export function parseCode(code: string) { - return (parseSync(code, { plugins: [babelJSX] })!.program.body[0] as t.ExpressionStatement) - .expression as AllowedJSXNode; -} - -export function parseView(node: AllowedJSXNode) { - return pV(node, config); -} - -export function parse(code: string) { - return parseView(parseCode(code)); -} - -export function wrapWithFile(node: t.Expression): t.File { - return t.file(t.program([t.expressionStatement(node)])); -} +import babel, { parseSync, types as t } from '@babel/core'; +import { AllowedJSXNode, ViewParserConfig } from '../types'; +import { parseView as pV } from '..'; +import babelJSX from '@babel/plugin-syntax-jsx'; + +const htmlTags = [ + 'a', + 'abbr', + 'address', + 'area', + 'article', + 'aside', + 'audio', + 'b', + 'base', + 'bdi', + 'bdo', + 'blockquote', + 'body', + 'br', + 'button', + 'canvas', + 'caption', + 'cite', + 'code', + 'col', + 'colgroup', + 'data', + 'datalist', + 'dd', + 'del', + 'details', + 'dfn', + 'dialog', + 'div', + 'dl', + 'dt', + 'em', + 'embed', + 'fieldset', + 'figcaption', + 'figure', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'header', + 'hgroup', + 'hr', + 'html', + 'i', + 'iframe', + 'img', + 'input', + 'ins', + 'kbd', + 'label', + 'legend', + 'li', + 'link', + 'main', + 'map', + 'mark', + 'menu', + 'meta', + 'meter', + 'nav', + 'noscript', + 'object', + 'ol', + 'optgroup', + 'option', + 'output', + 'p', + 'picture', + 'pre', + 'progress', + 'q', + 'rp', + 'rt', + 'ruby', + 's', + 'samp', + 'script', + 'section', + 'select', + 'slot', + 'small', + 'source', + 'span', + 'strong', + 'style', + 'sub', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'template', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'time', + 'title', + 'tr', + 'track', + 'u', + 'ul', + 'var', + 'video', + 'wbr', + 'acronym', + 'applet', + 'basefont', + 'bgsound', + 'big', + 'blink', + 'center', + 'dir', + 'font', + 'frame', + 'frameset', + 'isindex', + 'keygen', + 'listing', + 'marquee', + 'menuitem', + 'multicol', + 'nextid', + 'nobr', + 'noembed', + 'noframes', + 'param', + 'plaintext', + 'rb', + 'rtc', + 'spacer', + 'strike', + 'tt', + 'xmp', + 'animate', + 'animateMotion', + 'animateTransform', + 'circle', + 'clipPath', + 'defs', + 'desc', + 'ellipse', + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', + 'filter', + 'foreignObject', + 'g', + 'image', + 'line', + 'linearGradient', + 'marker', + 'mask', + 'metadata', + 'mpath', + 'path', + 'pattern', + 'polygon', + 'polyline', + 'radialGradient', + 'rect', + 'set', + 'stop', + 'svg', + 'switch', + 'symbol', + 'text', + 'textPath', + 'tspan', + 'use', + 'view', +]; + +export const config: ViewParserConfig = { + babelApi: babel, + htmlTags, +}; + +export function parseCode(code: string) { + return (parseSync(code, { plugins: [babelJSX] })!.program.body[0] as t.ExpressionStatement) + .expression as AllowedJSXNode; +} + +export function parseView(node: AllowedJSXNode) { + return pV(node, config); +} + +export function parse(code: string) { + return parseView(parseCode(code)); +} + +export function wrapWithFile(node: t.Expression): t.File { + return t.file(t.program([t.expressionStatement(node)])); +} diff --git a/packages/transpiler/jsx-parser/src/types.ts b/packages/transpiler/jsx-parser/src/types.ts index faba4f01dae387cef6c1a17295a06d70c7a40dcf..5188d8a5c1222e140012d699c59f09ef60691845 100644 --- a/packages/transpiler/jsx-parser/src/types.ts +++ b/packages/transpiler/jsx-parser/src/types.ts @@ -1,88 +1,88 @@ -import type Babel from '@babel/core'; -import type { types as t } from '@babel/core'; - -export interface Context { - ifElseStack: IfUnit[]; -} -export interface UnitProp { - value: t.Expression; - viewPropMap: Record; - specifier?: string; -} - -export interface TextUnit { - type: 'text'; - content: t.Literal; -} - -export type MutableUnit = ViewUnit & { path: number[] }; - -export interface TemplateProp { - tag: t.Expression; - name: string; - key: string; - path: number[]; - value: t.Expression; -} - -export interface TemplateUnit { - type: 'template'; - template: HTMLUnit; - mutableUnits: MutableUnit[]; - props: TemplateProp[]; -} - -export interface HTMLUnit { - type: 'html'; - tag: t.Expression; - props: Record; - children: ViewUnit[]; -} - -export interface CompUnit { - type: 'comp'; - tag: t.Expression; - props: Record; - children: ViewUnit[]; -} - -export interface IfBranch { - condition: t.Expression; - children: ViewUnit[]; -} - -export interface IfUnit { - type: 'if'; - branches: IfBranch[]; -} - -export interface ExpUnit { - type: 'exp'; - content: UnitProp; - props: Record; -} - -export interface ContextUnit { - type: 'context'; - props: Record; - children: ViewUnit[]; - contextName: string; -} - -export interface ForUnit { - type: 'for'; - item: t.LVal; - array: t.Expression; - key: t.Expression; - index: t.Identifier | null; - children: ViewUnit[]; -} -export type ViewUnit = TextUnit | HTMLUnit | CompUnit | IfUnit | ExpUnit | ContextUnit | TemplateUnit | ForUnit; - -export interface ViewParserConfig { - babelApi: typeof Babel; - htmlTags: string[]; - parseTemplate?: boolean; -} - -export type AllowedJSXNode = t.JSXElement | t.JSXFragment | t.JSXText | t.JSXExpressionContainer | t.JSXSpreadChild; +import type Babel from '@babel/core'; +import type { types as t } from '@babel/core'; + +export interface Context { + ifElseStack: IfUnit[]; +} +export interface UnitProp { + value: t.Expression; + viewPropMap: Record; + specifier?: string; +} + +export interface TextUnit { + type: 'text'; + content: t.Literal; +} + +export type MutableUnit = ViewUnit & { path: number[] }; + +export interface TemplateProp { + tag: t.Expression; + name: string; + key: string; + path: number[]; + value: t.Expression; +} + +export interface TemplateUnit { + type: 'template'; + template: HTMLUnit; + mutableUnits: MutableUnit[]; + props: TemplateProp[]; +} + +export interface HTMLUnit { + type: 'html'; + tag: t.Expression; + props: Record; + children: ViewUnit[]; +} + +export interface CompUnit { + type: 'comp'; + tag: t.Expression; + props: Record; + children: ViewUnit[]; +} + +export interface IfBranch { + condition: t.Expression; + children: ViewUnit[]; +} + +export interface IfUnit { + type: 'if'; + branches: IfBranch[]; +} + +export interface ExpUnit { + type: 'exp'; + content: UnitProp; + props: Record; +} + +export interface ContextUnit { + type: 'context'; + props: Record; + children: ViewUnit[]; + contextName: string; +} + +export interface ForUnit { + type: 'for'; + item: t.LVal; + array: t.Expression; + key: t.Expression; + index: t.Identifier | null; + children: ViewUnit[]; +} +export type ViewUnit = TextUnit | HTMLUnit | CompUnit | IfUnit | ExpUnit | ContextUnit | TemplateUnit | ForUnit; + +export interface ViewParserConfig { + babelApi: typeof Babel; + htmlTags: string[]; + parseTemplate?: boolean; +} + +export type AllowedJSXNode = t.JSXElement | t.JSXFragment | t.JSXText | t.JSXExpressionContainer | t.JSXSpreadChild; diff --git a/packages/transpiler/jsx-parser/src/utils.ts b/packages/transpiler/jsx-parser/src/utils.ts index 959966b47dc174d9bc0c353d5f91e0b4cc9b8c2c..9493bbff7b7ffe134755729507792947ee1f0556 100644 --- a/packages/transpiler/jsx-parser/src/utils.ts +++ b/packages/transpiler/jsx-parser/src/utils.ts @@ -1,54 +1,54 @@ -import type { types as t } from '@babel/core'; - -/** - * Cleans a JSX text node by removing leading and trailing whitespace and newlines. - * It also collapses multiple lines into a single line, preserving one space between non-empty lines. - * - * @param {t.JSXText} node - The JSX text node to clean. - * @returns {string} The cleaned text. - * - * @example - *
    - * Hello - * World - *
    - * const jsxText = { type: "JSXText", value: "\n Hello\n World\n " }; - * console.log(cleanJSXText(jsxText)); // Outputs: "Hello World" - */ -export function cleanJSXText(node: t.JSXText): string { - const textLines = node.value.split(/\r\n|\n|\r/); - let indexOfLastNonEmptyLine = textLines.length - 1; - - // Find last non-empty line - for (let i = 0; i < textLines.length; i++) { - if (/[^ \t]/.test(textLines[i])) { - indexOfLastNonEmptyLine = i; - break; - } - } - - return textLines.reduce((cleanedText, currentLine, currentIndex) => { - // Replace tabs with spaces - let processedLine = currentLine.replace(/\t/g, ' '); - - // Trim start of line if not first line - if (currentIndex !== 0) { - processedLine = processedLine.replace(/^[ ]+/, ''); - } - - // Trim end of line if not last line - if (currentIndex !== textLines.length - 1) { - processedLine = processedLine.replace(/[ ]+$/, ''); - } - - if (processedLine) { - cleanedText += processedLine; - // Add space between non-empty lines - if (currentIndex !== indexOfLastNonEmptyLine) { - cleanedText += ' '; - } - } - - return cleanedText; - }, ''); -} +import type { types as t } from '@babel/core'; + +/** + * Cleans a JSX text node by removing leading and trailing whitespace and newlines. + * It also collapses multiple lines into a single line, preserving one space between non-empty lines. + * + * @param {t.JSXText} node - The JSX text node to clean. + * @returns {string} The cleaned text. + * + * @example + *
    + * Hello + * World + *
    + * const jsxText = { type: "JSXText", value: "\n Hello\n World\n " }; + * console.log(cleanJSXText(jsxText)); // Outputs: "Hello World" + */ +export function cleanJSXText(node: t.JSXText): string { + const textLines = node.value.split(/\r\n|\n|\r/); + let indexOfLastNonEmptyLine = textLines.length - 1; + + // Find last non-empty line + for (let i = 0; i < textLines.length; i++) { + if (/[^ \t]/.test(textLines[i])) { + indexOfLastNonEmptyLine = i; + break; + } + } + + return textLines.reduce((cleanedText, currentLine, currentIndex) => { + // Replace tabs with spaces + let processedLine = currentLine.replace(/\t/g, ' '); + + // Trim start of line if not first line + if (currentIndex !== 0) { + processedLine = processedLine.replace(/^[ ]+/, ''); + } + + // Trim end of line if not last line + if (currentIndex !== textLines.length - 1) { + processedLine = processedLine.replace(/[ ]+$/, ''); + } + + if (processedLine) { + cleanedText += processedLine; + // Add space between non-empty lines + if (currentIndex !== indexOfLastNonEmptyLine) { + cleanedText += ' '; + } + } + + return cleanedText; + }, ''); +} diff --git a/packages/transpiler/jsx-parser/tsconfig.json b/packages/transpiler/jsx-parser/tsconfig.json index e0932d7828a8e6b8f5b2c7752a24057e6461adf0..c003315788d1e0451295fc2f3330f040cbeb5b00 100644 --- a/packages/transpiler/jsx-parser/tsconfig.json +++ b/packages/transpiler/jsx-parser/tsconfig.json @@ -1,13 +1,13 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", - "strict": true, - "esModuleInterop": true - }, - "ts-node": { - "esm": true - } +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true + }, + "ts-node": { + "esm": true + } } \ No newline at end of file diff --git a/packages/transpiler/reactivity-parser/src/error.ts b/packages/transpiler/reactivity-parser/src/error.ts index 8a5b5695392a99e1aa3a8d271681d51d403e945a..7566682aabee026556df0568487e3c4e04a77a1e 100644 --- a/packages/transpiler/reactivity-parser/src/error.ts +++ b/packages/transpiler/reactivity-parser/src/error.ts @@ -1,5 +1,5 @@ -import { createErrorHandler } from '@openinula/error-handler'; - -export const DLError = createErrorHandler('ReactivityParser', { - 1: 'Invalid ViewUnit type', -}); +import { createErrorHandler } from '@openinula/error-handler'; + +export const DLError = createErrorHandler('ReactivityParser', { + 1: 'Invalid ViewUnit type', +}); diff --git a/packages/transpiler/reactivity-parser/src/getDependencies.ts b/packages/transpiler/reactivity-parser/src/getDependencies.ts index 3a27fa67b22a1fe5f228200ec6d543a12ccd31f0..0fafe0fc1f7e49ee53947acf4677b1a460b9d120 100644 --- a/packages/transpiler/reactivity-parser/src/getDependencies.ts +++ b/packages/transpiler/reactivity-parser/src/getDependencies.ts @@ -1,285 +1,285 @@ -/* - * 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 { NodePath } from '@babel/core'; -import { getBabelApi, types as t } from '@openinula/babel-api'; -import { Bitmap } from './types'; - -export type Dependency = { - dependenciesNode: t.ArrayExpression; - /** - * Only contains the bit of direct dependencies and not contains the bit of used variables - * So it's configured in pruneUnusedBit.ts - */ - depMask?: Bitmap; - /** - * The bitmap of each dependency - */ - allDepBits: Bitmap[]; -}; - -/** - * @brief Get all valid dependencies of a babel path - * @returns - * @param node - * @param reactiveMap - * @param reactivityFuncNames - */ -export function getDependenciesFromNode( - node: t.Expression | t.Statement, - reactiveMap: Map, - reactivityFuncNames: string[] -): Dependency { - // ---- Deps: console.log(count) - const depBitmaps: number[] = []; - // ---- Assign deps: count = 1 or count++ - let assignDepMask = 0; - const depNodes: Record = {}; - const wrappedNode = valueWrapper(node); - - getBabelApi().traverse(wrappedNode, { - Identifier: (innerPath: NodePath) => { - const propertyKey = innerPath.node.name; - const reactiveBitmap = reactiveMap.get(propertyKey); - - if (reactiveBitmap !== undefined) { - if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath, reactivityFuncNames)) { - // write - assignDepMask |= Array.isArray(reactiveBitmap) - ? reactiveBitmap.reduce((acc, cur) => acc | cur, 0) - : reactiveBitmap; - } else if ( - (isStandAloneIdentifier(innerPath) && !isMemberInUntrackFunction(innerPath)) || - isMemberOfMemberExpression(innerPath) - ) { - // read - if (Array.isArray(reactiveBitmap)) { - depBitmaps.push(...reactiveBitmap); - } else { - depBitmaps.push(reactiveBitmap); - } - - if (!depNodes[propertyKey]) depNodes[propertyKey] = []; - depNodes[propertyKey].push(geneDependencyNode(innerPath)); - } - } - }, - }); - - const fullDepMask = depBitmaps.reduce((acc, cur) => acc | cur, 0); - // ---- Eliminate deps that are assigned in the same method - // e.g. { console.log(count); count = 1 } - // this will cause infinite loop - // so we eliminate "count" from deps - if (assignDepMask & fullDepMask) { - // TODO: We should throw an error here to indicate the user that there is a loop - } - - // deduplicate the dependency nodes - let dependencyNodes = Object.values(depNodes).flat(); - // ---- deduplicate the dependency nodes - dependencyNodes = dependencyNodes.filter((n, i) => { - const idx = dependencyNodes.findIndex(m => t.isNodesEquivalent(m, n)); - return idx === i; - }); - - return { - dependenciesNode: t.arrayExpression(dependencyNodes as t.Expression[]), - allDepBits: depBitmaps, - }; -} - -/** - * @brief Check if it's the left side of an assignment expression, e.g. count = 1 - * @param innerPath - * @returns assignment expression - */ -function isAssignmentExpressionLeft(innerPath: NodePath): NodePath | null { - let parentPath = innerPath.parentPath; - while (parentPath && !parentPath.isStatement()) { - if (parentPath.isAssignmentExpression()) { - if (parentPath.node.left === innerPath.node) return parentPath; - const leftPath = parentPath.get('left') as NodePath; - if (innerPath.isDescendant(leftPath)) return parentPath; - } else if (parentPath.isUpdateExpression()) { - return parentPath; - } - parentPath = parentPath.parentPath; - } - - return null; -} - -/** - * @brief Check if it's a reactivity function, e.g. arr.push - * @param innerPath - * @param reactivityFuncNames - * @returns - */ -function isAssignmentFunction(innerPath: NodePath, reactivityFuncNames: string[]): boolean { - let parentPath = innerPath.parentPath; - - while (parentPath && parentPath.isMemberExpression()) { - parentPath = parentPath.parentPath; - } - if (!parentPath) return false; - return ( - parentPath.isCallExpression() && - parentPath.get('callee').isIdentifier() && - reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name) - ); -} - -function valueWrapper(node: t.Expression | t.Statement): t.File { - return t.file(t.program([t.isStatement(node) ? node : t.expressionStatement(node)])); -} - -/** - * @brief Generate a dependency node from a dependency identifier, - * loop until the parent node is not a binary expression or a member expression - * And turn the member expression into an optional member expression, like info.name -> info?.name - * @param path - * @returns - */ -function geneDependencyNode(path: NodePath): t.Node { - let parentPath = path; - while (parentPath?.parentPath) { - const pParentPath = parentPath.parentPath; - if ( - !(t.isMemberExpression(pParentPath.node, { computed: false }) || t.isOptionalMemberExpression(pParentPath.node)) - ) { - break; - } - parentPath = pParentPath; - } - const depNode = t.cloneNode(parentPath.node); - // ---- Turn memberExpression to optionalMemberExpression - getBabelApi().traverse(valueWrapper(depNode as t.Expression), { - MemberExpression: innerPath => { - if (t.isThisExpression(innerPath.node.object)) return; - innerPath.node.optional = true; - innerPath.node.type = 'OptionalMemberExpression' as any; - }, - }); - return depNode; -} - -function isMemberInUntrackFunction(innerPath: NodePath): boolean { - let isInFunction = false; - let reversePath = innerPath.parentPath; - while (reversePath) { - const node = reversePath.node; - if ( - t.isCallExpression(node) && - t.isIdentifier(node.callee) && - ['untrack', '$$untrack'].includes(node.callee.name) - ) { - isInFunction = true; - break; - } - reversePath = reversePath.parentPath; - } - return isInFunction; -} - -function isMemberOfMemberExpression(innerPath: NodePath): boolean { - if (innerPath.parentPath === null) { - return false; - } - return innerPath.parentPath.isMemberExpression() && innerPath.parentPath.node.property === innerPath.node; -} - -/** - * @brief Check if an identifier is a simple stand alone identifier, - * i.e., not a member expression, nor a function param - * @param path - * 1. not a member expression - * 2. not a function param - * 3. not in a declaration - * 4. not as object property's not computed key - * @returns is a standalone identifier - */ -function isStandAloneIdentifier(path: NodePath): boolean { - const node = path.node; - const parentNode = path.parentPath?.node; - const isMemberExpression = t.isMemberExpression(parentNode) && parentNode.property === node; - if (isMemberExpression) return false; - const isFunctionParam = isAttrFromFunction(path, node.name); - if (isFunctionParam) return false; - while (path.parentPath) { - if (t.isVariableDeclarator(path.parentPath.node)) return false; - if ( - t.isObjectProperty(path.parentPath.node) && - path.parentPath.node.key === path.node && - !path.parentPath.node.computed - ) - return false; - path = path.parentPath as NodePath; - } - return true; -} - -/** - * @brief check if the identifier is from a function param till the stopNode - * e.g: - * function myFunc1(ok) { // stopNode = functionBody - * const myFunc2 = ok => ok // from function param - * console.log(ok) // not from function param - * } - */ -function isAttrFromFunction(path: NodePath, idName: string) { - let reversePath = path.parentPath; - - function checkParam(param: t.Node): boolean { - // ---- 3 general types: - // * represent allow nesting - // ---0 Identifier: (a) - // ---1 RestElement: (...a) * - // ---1 Pattern: 3 sub Pattern - // -----0 AssignmentPattern: (a=1) * - // -----1 ArrayPattern: ([a, b]) * - // -----2 ObjectPattern: ({a, b}) - if (t.isIdentifier(param)) return param.name === idName; - if (t.isAssignmentPattern(param)) return checkParam(param.left); - if (t.isArrayPattern(param)) { - return param.elements - .filter(Boolean) - .map(el => checkParam(el!)) - .includes(true); - } - if (t.isObjectPattern(param)) { - return ( - param.properties.filter(prop => t.isObjectProperty(prop) && t.isIdentifier(prop.key)) as t.ObjectProperty[] - ) - .map(prop => (prop.key as t.Identifier).name) - .includes(idName); - } - if (t.isRestElement(param)) return checkParam(param.argument); - - return false; - } - - while (reversePath) { - const node = reversePath.node; - if (t.isArrowFunctionExpression(node) || t.isFunctionDeclaration(node)) { - for (const param of node.params) { - if (checkParam(param)) return true; - } - } - reversePath = reversePath.parentPath; - } - - return false; -} +/* + * 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 { NodePath } from '@babel/core'; +import { getBabelApi, types as t } from '@openinula/babel-api'; +import { Bitmap } from './types'; + +export type Dependency = { + dependenciesNode: t.ArrayExpression; + /** + * Only contains the bit of direct dependencies and not contains the bit of used variables + * So it's configured in pruneUnusedBit.ts + */ + depMask?: Bitmap; + /** + * The bitmap of each dependency + */ + allDepBits: Bitmap[]; +}; + +/** + * @brief Get all valid dependencies of a babel path + * @returns + * @param node + * @param reactiveMap + * @param reactivityFuncNames + */ +export function getDependenciesFromNode( + node: t.Expression | t.Statement, + reactiveMap: Map, + reactivityFuncNames: string[] +): Dependency { + // ---- Deps: console.log(count) + const depBitmaps: number[] = []; + // ---- Assign deps: count = 1 or count++ + let assignDepMask = 0; + const depNodes: Record = {}; + const wrappedNode = valueWrapper(node); + + getBabelApi().traverse(wrappedNode, { + Identifier: (innerPath: NodePath) => { + const propertyKey = innerPath.node.name; + const reactiveBitmap = reactiveMap.get(propertyKey); + + if (reactiveBitmap !== undefined) { + if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath, reactivityFuncNames)) { + // write + assignDepMask |= Array.isArray(reactiveBitmap) + ? reactiveBitmap.reduce((acc, cur) => acc | cur, 0) + : reactiveBitmap; + } else if ( + (isStandAloneIdentifier(innerPath) && !isMemberInUntrackFunction(innerPath)) || + isMemberOfMemberExpression(innerPath) + ) { + // read + if (Array.isArray(reactiveBitmap)) { + depBitmaps.push(...reactiveBitmap); + } else { + depBitmaps.push(reactiveBitmap); + } + + if (!depNodes[propertyKey]) depNodes[propertyKey] = []; + depNodes[propertyKey].push(geneDependencyNode(innerPath)); + } + } + }, + }); + + const fullDepMask = depBitmaps.reduce((acc, cur) => acc | cur, 0); + // ---- Eliminate deps that are assigned in the same method + // e.g. { console.log(count); count = 1 } + // this will cause infinite loop + // so we eliminate "count" from deps + if (assignDepMask & fullDepMask) { + // TODO: We should throw an error here to indicate the user that there is a loop + } + + // deduplicate the dependency nodes + let dependencyNodes = Object.values(depNodes).flat(); + // ---- deduplicate the dependency nodes + dependencyNodes = dependencyNodes.filter((n, i) => { + const idx = dependencyNodes.findIndex(m => t.isNodesEquivalent(m, n)); + return idx === i; + }); + + return { + dependenciesNode: t.arrayExpression(dependencyNodes as t.Expression[]), + allDepBits: depBitmaps, + }; +} + +/** + * @brief Check if it's the left side of an assignment expression, e.g. count = 1 + * @param innerPath + * @returns assignment expression + */ +function isAssignmentExpressionLeft(innerPath: NodePath): NodePath | null { + let parentPath = innerPath.parentPath; + while (parentPath && !parentPath.isStatement()) { + if (parentPath.isAssignmentExpression()) { + if (parentPath.node.left === innerPath.node) return parentPath; + const leftPath = parentPath.get('left') as NodePath; + if (innerPath.isDescendant(leftPath)) return parentPath; + } else if (parentPath.isUpdateExpression()) { + return parentPath; + } + parentPath = parentPath.parentPath; + } + + return null; +} + +/** + * @brief Check if it's a reactivity function, e.g. arr.push + * @param innerPath + * @param reactivityFuncNames + * @returns + */ +function isAssignmentFunction(innerPath: NodePath, reactivityFuncNames: string[]): boolean { + let parentPath = innerPath.parentPath; + + while (parentPath && parentPath.isMemberExpression()) { + parentPath = parentPath.parentPath; + } + if (!parentPath) return false; + return ( + parentPath.isCallExpression() && + parentPath.get('callee').isIdentifier() && + reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name) + ); +} + +function valueWrapper(node: t.Expression | t.Statement): t.File { + return t.file(t.program([t.isStatement(node) ? node : t.expressionStatement(node)])); +} + +/** + * @brief Generate a dependency node from a dependency identifier, + * loop until the parent node is not a binary expression or a member expression + * And turn the member expression into an optional member expression, like info.name -> info?.name + * @param path + * @returns + */ +function geneDependencyNode(path: NodePath): t.Node { + let parentPath = path; + while (parentPath?.parentPath) { + const pParentPath = parentPath.parentPath; + if ( + !(t.isMemberExpression(pParentPath.node, { computed: false }) || t.isOptionalMemberExpression(pParentPath.node)) + ) { + break; + } + parentPath = pParentPath; + } + const depNode = t.cloneNode(parentPath.node); + // ---- Turn memberExpression to optionalMemberExpression + getBabelApi().traverse(valueWrapper(depNode as t.Expression), { + MemberExpression: innerPath => { + if (t.isThisExpression(innerPath.node.object)) return; + innerPath.node.optional = true; + innerPath.node.type = 'OptionalMemberExpression' as any; + }, + }); + return depNode; +} + +function isMemberInUntrackFunction(innerPath: NodePath): boolean { + let isInFunction = false; + let reversePath = innerPath.parentPath; + while (reversePath) { + const node = reversePath.node; + if ( + t.isCallExpression(node) && + t.isIdentifier(node.callee) && + ['untrack', '$$untrack'].includes(node.callee.name) + ) { + isInFunction = true; + break; + } + reversePath = reversePath.parentPath; + } + return isInFunction; +} + +function isMemberOfMemberExpression(innerPath: NodePath): boolean { + if (innerPath.parentPath === null) { + return false; + } + return innerPath.parentPath.isMemberExpression() && innerPath.parentPath.node.property === innerPath.node; +} + +/** + * @brief Check if an identifier is a simple stand alone identifier, + * i.e., not a member expression, nor a function param + * @param path + * 1. not a member expression + * 2. not a function param + * 3. not in a declaration + * 4. not as object property's not computed key + * @returns is a standalone identifier + */ +function isStandAloneIdentifier(path: NodePath): boolean { + const node = path.node; + const parentNode = path.parentPath?.node; + const isMemberExpression = t.isMemberExpression(parentNode) && parentNode.property === node; + if (isMemberExpression) return false; + const isFunctionParam = isAttrFromFunction(path, node.name); + if (isFunctionParam) return false; + while (path.parentPath) { + if (t.isVariableDeclarator(path.parentPath.node)) return false; + if ( + t.isObjectProperty(path.parentPath.node) && + path.parentPath.node.key === path.node && + !path.parentPath.node.computed + ) + return false; + path = path.parentPath as NodePath; + } + return true; +} + +/** + * @brief check if the identifier is from a function param till the stopNode + * e.g: + * function myFunc1(ok) { // stopNode = functionBody + * const myFunc2 = ok => ok // from function param + * console.log(ok) // not from function param + * } + */ +function isAttrFromFunction(path: NodePath, idName: string) { + let reversePath = path.parentPath; + + function checkParam(param: t.Node): boolean { + // ---- 3 general types: + // * represent allow nesting + // ---0 Identifier: (a) + // ---1 RestElement: (...a) * + // ---1 Pattern: 3 sub Pattern + // -----0 AssignmentPattern: (a=1) * + // -----1 ArrayPattern: ([a, b]) * + // -----2 ObjectPattern: ({a, b}) + if (t.isIdentifier(param)) return param.name === idName; + if (t.isAssignmentPattern(param)) return checkParam(param.left); + if (t.isArrayPattern(param)) { + return param.elements + .filter(Boolean) + .map(el => checkParam(el!)) + .includes(true); + } + if (t.isObjectPattern(param)) { + return ( + param.properties.filter(prop => t.isObjectProperty(prop) && t.isIdentifier(prop.key)) as t.ObjectProperty[] + ) + .map(prop => (prop.key as t.Identifier).name) + .includes(idName); + } + if (t.isRestElement(param)) return checkParam(param.argument); + + return false; + } + + while (reversePath) { + const node = reversePath.node; + if (t.isArrowFunctionExpression(node) || t.isFunctionDeclaration(node)) { + for (const param of node.params) { + if (checkParam(param)) return true; + } + } + reversePath = reversePath.parentPath; + } + + return false; +} diff --git a/packages/transpiler/reactivity-parser/src/index.ts b/packages/transpiler/reactivity-parser/src/index.ts index c0b236a32cfed0c250f43ddf92e84c7faa714014..3868a4c3daadb8c47efabfe6f2c2df653ecb18c5 100644 --- a/packages/transpiler/reactivity-parser/src/index.ts +++ b/packages/transpiler/reactivity-parser/src/index.ts @@ -1,25 +1,25 @@ -import { type ViewUnit } from '@openinula/jsx-view-parser'; -import { ReactivityParser } from './parser'; -import { type ViewParticle, type ReactivityParserConfig } from './types'; - -/** - * @brief Parse view units to get used properties and view particles with reactivity - * @param viewUnits - * @param config - * @returns [viewParticles, usedProperties] - */ -export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserConfig): [ViewParticle[], number] { - // ---- ReactivityParser only accepts one view unit at a time, - // so we loop through the view units and get all the used properties - let usedBit = 0; - const dlParticles = viewUnits.map(viewUnit => { - const parser = new ReactivityParser(config); - const dlParticle = parser.parse(viewUnit); - usedBit |= parser.usedBit; - return dlParticle; - }); - return [dlParticles, usedBit]; -} - -export { getDependenciesFromNode } from './getDependencies'; -export * from './types'; +import { type ViewUnit } from '@openinula/jsx-view-parser'; +import { ReactivityParser } from './parser'; +import { type ViewParticle, type ReactivityParserConfig } from './types'; + +/** + * @brief Parse view units to get used properties and view particles with reactivity + * @param viewUnits + * @param config + * @returns [viewParticles, usedProperties] + */ +export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserConfig): [ViewParticle[], number] { + // ---- ReactivityParser only accepts one view unit at a time, + // so we loop through the view units and get all the used properties + let usedBit = 0; + const particles = viewUnits.map(viewUnit => { + const parser = new ReactivityParser(config); + const particle = parser.parse(viewUnit); + usedBit |= parser.usedBit; + return particle; + }); + return [particles, usedBit]; +} + +export { getDependenciesFromNode } from './getDependencies'; +export * from './types'; diff --git a/packages/transpiler/reactivity-parser/src/parser.ts b/packages/transpiler/reactivity-parser/src/parser.ts index 752291d8d741aa4a51f01697761d29701bff31b5..c809a51233934460cbae0933c4e52c20f6318f69 100644 --- a/packages/transpiler/reactivity-parser/src/parser.ts +++ b/packages/transpiler/reactivity-parser/src/parser.ts @@ -1,621 +1,621 @@ -import { - type CompParticle, - type DependencyProp, - DepMaskMap, - type ContextParticle, - type ExpParticle, - type ForParticle, - type HTMLParticle, - type IfParticle, - type MutableParticle, - type ReactivityParserConfig, - type TemplateParticle, - type TemplateProp, - type TextParticle, - type ViewParticle, -} from './types'; -import { type NodePath, type traverse, type types as t } from '@babel/core'; -import { - type CompUnit, - type ContextUnit, - type ExpUnit, - type ForUnit, - type HTMLUnit, - type IfUnit, - type TextUnit, - type UnitProp, - type ViewUnit, -} from '@openinula/jsx-view-parser'; -import { DLError } from './error'; -import { Dependency, getDependenciesFromNode } from './getDependencies'; - -export class ReactivityParser { - private readonly config: ReactivityParserConfig; - - private readonly t: typeof t; - private readonly traverse: typeof traverse; - private readonly depMaskMap: DepMaskMap; - private readonly reactivityFuncNames; - - private static readonly customHTMLProps = [ - 'didUpdate', - 'willMount', - 'didMount', - 'willUnmount', - 'didUnmount', - 'element', - 'innerHTML', - 'props', - 'attrs', - 'dataset', - 'forwardProps', - ]; - - readonly usedProperties = new Set(); - usedBit = 0; - - /** - * @brief Constructor - * @param config - */ - constructor(config: ReactivityParserConfig) { - this.config = config; - this.t = config.babelApi.types; - this.traverse = config.babelApi.traverse; - this.depMaskMap = config.depMaskMap; - this.reactivityFuncNames = config.reactivityFuncNames ?? []; - } - - /** - * @brief Parse the ViewUnit into a ViewParticle - * @returns - */ - parse(viewUnit: ViewUnit): ViewParticle { - return this.parseViewUnit(viewUnit); - } - - /** - * @brief Parse a ViewUnit into a ViewParticle - * @param viewUnit - * @returns ViewParticle - */ - private parseViewUnit(viewUnit: ViewUnit): ViewParticle { - if (this.isHTMLTemplate(viewUnit)) return this.parseTemplate(viewUnit as HTMLUnit); - if (viewUnit.type === 'text') return this.parseText(viewUnit); - if (viewUnit.type === 'html') return this.parseHTML(viewUnit); - if (viewUnit.type === 'comp') return this.parseComp(viewUnit); - if (viewUnit.type === 'for') return this.parseFor(viewUnit); - if (viewUnit.type === 'if') return this.parseIf(viewUnit); - if (viewUnit.type === 'context') return this.parseContext(viewUnit); - if (viewUnit.type === 'exp') return this.parseExp(viewUnit); - return DLError.throw1(); - } - - // ---- Parsers ---- - // ---- @Template ---- - /** - * @brief Collect static HTMLUnit into a template particle and generate a template string - * MutableParticle means whatever unit that is not a static HTMLUnit or a TextUnit - * Props means all the non-static props of the nested HTMLUnit or TextUnit, e.g. div().className(this.name) - * @param htmlUnit - * @returns TemplateParticle - */ - private parseTemplate(htmlUnit: HTMLUnit): TemplateParticle { - return { - type: 'template', - template: this.generateTemplate(htmlUnit), - props: this.parseTemplateProps(htmlUnit), - mutableParticles: this.generateMutableParticles(htmlUnit), - }; - } - - /** - * @brief Generate a template - * There'll be a situation where the tag is dynamic, e.g. tag(this.htmlTag), - * which we can't generate a template string for it, so we'll wrap it in an ExpParticle in parseHTML() section - * @returns template string - * @param unit - */ - private generateTemplate(unit: HTMLUnit): HTMLParticle { - const staticProps = this.filterTemplateProps( - // ---- Get all the static props - Object.entries(unit.props).filter( - ([, prop]) => - this.isStaticProp(prop) && - // ---- Filter out props with false values - !(this.t.isBooleanLiteral(prop.value) && !prop.value.value) - ) - ).map(([key, prop]) => [ - key, - { - ...prop, - depMask: 0, - allDepBits: [], - dependenciesNode: this.t.arrayExpression([]), - }, - ]); - - let children: ViewParticle[] = []; - if (!unit.props.textContent) { - children = unit.children - .map(unit => { - if (unit.type === 'html' && this.t.isStringLiteral(unit.tag)) { - return this.generateTemplate(unit); - } - if (unit.type === 'text' && this.t.isStringLiteral(unit.content)) { - return this.parseText(unit); - } - }) - .filter(Boolean) as HTMLParticle[]; - } - return { - type: 'html', - tag: unit.tag, - props: Object.fromEntries(staticProps), - children, - }; - } - - /** - * @brief Collect all the mutable nodes in a static HTMLUnit - * We use this function to collect mutable nodes' path and props, - * so that in the generate, we know which position to insert the mutable nodes - * @param htmlUnit - * @returns mutable particles - */ - private generateMutableParticles(htmlUnit: HTMLUnit): MutableParticle[] { - const mutableParticles: MutableParticle[] = []; - const generateMutableUnit = (unit: HTMLUnit, path: number[] = []) => { - // ---- Generate mutable particles for current HTMLUnit - unit.children?.forEach((child, idx) => { - if ( - !(child.type === 'html' && this.t.isStringLiteral(child.tag)) && - !(child.type === 'text' && this.t.isStringLiteral(child.content)) - ) { - mutableParticles.push({ - path: [...path, idx], - ...this.parseViewParticle(child), - }); - } - }); - // ---- Recursively generate mutable particles for static HTMLUnit children - unit.children - ?.filter( - child => - (child.type === 'html' && this.t.isStringLiteral(child.tag)) || - (child.type === 'text' && this.t.isStringLiteral(child.content)) - ) - .forEach((child, idx) => { - if (child.type === 'html') { - generateMutableUnit(child as HTMLUnit, [...path, idx]); - } - }); - }; - generateMutableUnit(htmlUnit); - - return mutableParticles; - } - - /** - * @brief Collect all the props in a static HTMLUnit or its nested HTMLUnit or TextUnit children - * Just like the mutable nodes, props are also equipped with path, - * so that we know which HTML ChildNode to insert the props - * @param htmlUnit - * @returns props - */ - private parseTemplateProps(htmlUnit: HTMLUnit): TemplateProp[] { - const templateProps: TemplateProp[] = []; - const generateVariableProp = (unit: HTMLUnit, path: number[]) => { - // ---- Generate all non-static(string/number/boolean) props for current HTMLUnit - // to be inserted further in the generate - Object.entries(unit.props) - .filter(([, prop]) => !this.isStaticProp(prop)) - .forEach(([key, prop]) => { - templateProps.push({ - tag: (unit.tag as t.StringLiteral).value, - key, - path, - value: prop.value, - ...this.getDependencies(prop.value), - }); - }); - // ---- Recursively generate props for static HTMLUnit children - unit.children - .filter( - child => - (child.type === 'html' && this.t.isStringLiteral(child.tag)) || - (child.type === 'text' && this.t.isStringLiteral(child.content)) - ) - .forEach((child, idx) => { - if (child.type === 'html') { - generateVariableProp(child, [...path, idx]); - } else if (child.type === 'text') { - // ---- if the child is a TextUnit, we just insert the text content - templateProps.push({ - tag: 'text', - key: 'value', - path: [...path, idx], - value: child.content, - depMask: 0, - dependenciesNode: this.t.arrayExpression([]), - allDepBits: [], - }); - } - }); - }; - generateVariableProp(htmlUnit, []); - - return templateProps; - } - - // ---- @Text ---- - /** - * @brief Parse a TextUnit into a TextParticle. - * This is only for a top level TextUnit, because if nested in HTMLUnit, it'll be parsed in the template string - * @param textUnit - * @returns TextParticle - */ - private parseText(textUnit: TextUnit): TextParticle { - return { - type: 'text', - content: { - value: textUnit.content, - ...this.getDependencies(textUnit.content), - }, - }; - } - - // ---- @HTML ---- - /** - * @brief Parse an HTMLUnit with a dynamic tag into an ExpParticle or an HTMLParticle - * We detect dependencies in the tag, if there's no dependency, - * we parse it as an HTMLParticle and dynamically append it to the parent node; - * if there's dependency, we parse it as an ExpParticle and wrap it in an ExpParticle - * so that we can make the tag reactive - * @param htmlUnit - * @returns ExpParticle | HTMLParticle - */ - private parseHTML(htmlUnit: HTMLUnit): ExpParticle | HTMLParticle { - const innerHTMLParticle: HTMLParticle = { - type: 'html', - tag: htmlUnit.tag, - props: {}, - children: [], - }; - - innerHTMLParticle.props = Object.fromEntries( - Object.entries(htmlUnit.props).map(([key, prop]) => [key, this.generateDependencyProp(prop)]) - ); - - innerHTMLParticle.children = htmlUnit.children.map(this.parseViewParticle.bind(this)); - - // ---- Not a dynamic tag - return innerHTMLParticle; - } - - // ---- @Comp ---- - /** - * @brief Parse a CompUnit into a CompParticle or an ExpParticle - * Similar to parseHTML(), we detect dependencies in the tag, if there's no dependency, - * we parse it as a regular CompParticle, otherwise we wrap it with an ExpParticle. - * @param compUnit - * @returns CompParticle | ExpParticle - */ - private parseComp(compUnit: CompUnit): CompParticle { - const compParticle: CompParticle = { - type: 'comp', - tag: compUnit.tag, - props: {}, - children: [], - }; - - compParticle.props = Object.fromEntries( - Object.entries(compUnit.props).map(([key, prop]) => [key, this.generateDependencyProp(prop)]) - ); - compParticle.children = compUnit.children.map(this.parseViewParticle.bind(this)); - - return compParticle; - } - - // ---- @For ---- - /** - * @brief Parse a ForUnit into a ForParticle with dependencies - * Key and item doesn't need to be reactive, so here we don't collect dependencies for it - * @param forUnit - * @returns ForParticle - */ - private parseFor(forUnit: ForUnit): ForParticle { - const { dependenciesNode, allDepBits } = this.getDependencies(forUnit.array); - const prevMap = this.config.depMaskMap; - - // ---- Generate an identifierDepMap to track identifiers in item and make them reactive - // based on the dependencies from the array - // Just wrap the item in an assignment expression to get all the identifiers - const itemWrapper = this.t.assignmentExpression('=', forUnit.item, this.t.objectExpression([])); - this.config.depMaskMap = new Map([ - ...this.config.depMaskMap, - ...this.getIdentifiers(itemWrapper).map(id => [id, allDepBits] as const), - ]); - - const forParticle: ForParticle = { - type: 'for', - item: forUnit.item, - index: forUnit.index, - array: { - value: forUnit.array, - allDepBits, - dependenciesNode, - }, - children: forUnit.children.map(this.parseViewParticle.bind(this)), - key: forUnit.key, - }; - this.config.depMaskMap = prevMap; - return forParticle; - } - - // ---- @If ---- - /** - * @brief Parse an IfUnit into an IfParticle with dependencies - * @param ifUnit - * @returns IfParticle - */ - private parseIf(ifUnit: IfUnit): IfParticle { - return { - type: 'if', - branches: ifUnit.branches.map(branch => ({ - condition: { - value: branch.condition, - ...this.getDependencies(branch.condition), - }, - children: branch.children.map(this.parseViewParticle.bind(this)), - })), - }; - } - - // ---- @Env ---- - /** - * @brief Parse an EnvUnit into an EnvParticle with dependencies - * @param contextProviderUnit - * @returns ContextParticle - */ - private parseContext(contextProviderUnit: ContextUnit): ContextParticle { - return { - type: 'context', - contextName: contextProviderUnit.contextName, - props: Object.fromEntries( - Object.entries(contextProviderUnit.props).map(([key, prop]) => [key, this.generateDependencyProp(prop)]) - ), - children: contextProviderUnit.children.map(this.parseViewParticle.bind(this)), - }; - } - - // ---- @Exp ---- - /** - * @brief Parse an ExpUnit into an ExpParticle with dependencies - * @param expUnit - * @returns ExpParticle - */ - private parseExp(expUnit: ExpUnit): ExpParticle { - return { - type: 'exp', - content: this.generateDependencyProp(expUnit.content), - }; - } - - // ---- Dependencies ---- - /** - * @brief Generate a dependency prop with dependencies - * @param prop - * @returns DependencyProp - */ - private generateDependencyProp(prop: UnitProp): DependencyProp { - return { - value: prop.value, - ...this.getDependencies(prop.value), - viewPropMap: Object.fromEntries( - Object.entries(prop.viewPropMap).map(([key, units]) => [key, units.map(this.parseViewParticle.bind(this))]) - ), - }; - } - - /** - * @brief Get all the dependencies of a node - * this.dependencyParseType controls how we parse the dependencies - * 1. property: parse the dependencies of a node as a property, e.g. this.name - * 2. identifier: parse the dependencies of a node as an identifier, e.g. name - * The availableProperties is the list of all the properties that can be used in the template, - * no matter it's a property or an identifier - * @param node - * @returns dependency index array - */ - private getDependencies(node: t.Expression | t.Statement): Dependency { - if (this.t.isFunctionExpression(node) || this.t.isArrowFunctionExpression(node)) { - return { - allDepBits: [], - dependenciesNode: this.t.arrayExpression([]), - }; - } - // ---- Both id and prop deps need to be calculated because - // id is for snippet update, prop is normal update - // in a snippet, the depsNode should be both id and prop - const dependency = getDependenciesFromNode(node, this.depMaskMap, this.reactivityFuncNames); - this.usedBit |= dependency.allDepBits.reduce((acc, cur) => acc | cur, 0); - return dependency; - } - - // ---- Utils ---- - /** - * @brief Parse a ViewUnit into a ViewParticle by new-ing a ReactivityParser - * @param viewUnit - * @returns ViewParticle - */ - private parseViewParticle(viewUnit: ViewUnit): ViewParticle { - const parser = new ReactivityParser(this.config); - const parsedUnit = parser.parse(viewUnit); - // ---- Collect used properties - parser.usedProperties.forEach(this.usedProperties.add.bind(this.usedProperties)); - this.usedBit |= parser.usedBit; - return parsedUnit; - } - - /** - * @brief Check if a ViewUnit is a static HTMLUnit that can be parsed into a template - * Must satisfy: - * 1. type is html - * 2. tag is a string literal, i.e., non-dynamic tag - * 3. has at least one child that is a static HTMLUnit, - * or else just call a createElement function, no need for template clone - * @param viewUnit - * @returns is a static HTMLUnit - */ - private isHTMLTemplate(viewUnit: ViewUnit): boolean { - return ( - viewUnit.type === 'html' && - this.t.isStringLiteral(viewUnit.tag) && - !!viewUnit.children?.some(child => child.type === 'html' && this.t.isStringLiteral(child.tag)) - ); - } - - /** - * @brief Check if a prop is a static prop - * i.e. - * 1. no viewPropMap - * 2. value is a string/number/boolean literal - * @param prop - * @returns is a static prop - */ - private isStaticProp(prop: UnitProp): boolean { - const { value, viewPropMap } = prop; - return ( - (!viewPropMap || Object.keys(viewPropMap).length === 0) && - (this.t.isStringLiteral(value) || this.t.isNumericLiteral(value) || this.t.isBooleanLiteral(value)) - ); - } - - /** - * @brief Filter out some props that are not needed in the template, - * these are all special props to be parsed differently in the generate - * @param props - * @returns filtered props - */ - private filterTemplateProps(props: Array<[string, T]>): Array<[string, T]> { - return ( - props - // ---- Filter out event listeners - .filter(([key]) => !key.startsWith('on')) - // ---- Filter out specific props - .filter(([key]) => !ReactivityParser.customHTMLProps.includes(key)) - ); - } - - /** - * @brief Wrap the value in a file - * @param node - * @returns wrapped value - */ - private valueWrapper(node: t.Expression | t.Statement): t.File { - return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)])); - } - - /** - * @brief Check if an identifier is a simple stand alone identifier, - * i.e., not a member expression, nor a function param - * @param path - * 1. not a member expression - * 2. not a function param - * 3. not in a declaration - * 4. not as object property's not computed key - * @returns is a stand alone identifier - */ - private isStandAloneIdentifier(path: NodePath): boolean { - const node = path.node; - const parentNode = path.parentPath?.node; - const isMemberExpression = this.t.isMemberExpression(parentNode) && parentNode.property === node; - if (isMemberExpression) return false; - const isFunctionParam = this.isAttrFromFunction(path, node.name); - if (isFunctionParam) return false; - while (path.parentPath) { - if (this.t.isVariableDeclarator(path.parentPath.node)) return false; - if ( - this.t.isObjectProperty(path.parentPath.node) && - path.parentPath.node.key === path.node && - !path.parentPath.node.computed - ) - return false; - path = path.parentPath as NodePath; - } - return true; - } - - /** - * @brief Get all identifiers as strings in a node - * @param node - * @returns identifiers - */ - private getIdentifiers(node: t.Node): string[] { - if (this.t.isIdentifier(node)) return [node.name]; - const identifierKeys = new Set(); - this.traverse(this.valueWrapper(node as t.Expression), { - Identifier: innerPath => { - if (!this.isStandAloneIdentifier(innerPath)) return; - identifierKeys.add(innerPath.node.name); - }, - }); - return [...identifierKeys]; - } - - /** - * @brief check if the identifier is from a function param till the stopNode - * e.g: - * function myFunc1(ok) { // stopNode = functionBody - * const myFunc2 = ok => ok // from function param - * console.log(ok) // not from function param - * } - */ - private isAttrFromFunction(path: NodePath, idName: string) { - let reversePath = path.parentPath; - - const checkParam: (param: t.Node) => boolean = (param: t.Node) => { - // ---- 3 general types: - // * represent allow nesting - // ---0 Identifier: (a) - // ---1 RestElement: (...a) * - // ---1 Pattern: 3 sub Pattern - // -----0 AssignmentPattern: (a=1) * - // -----1 ArrayPattern: ([a, b]) * - // -----2 ObjectPattern: ({a, b}) - if (this.t.isIdentifier(param)) return param.name === idName; - if (this.t.isAssignmentPattern(param)) return checkParam(param.left); - if (this.t.isArrayPattern(param)) { - return param.elements - .filter(Boolean) - .map(el => checkParam(el!)) - .includes(true); - } - if (this.t.isObjectPattern(param)) { - return ( - param.properties.filter( - prop => this.t.isObjectProperty(prop) && this.t.isIdentifier(prop.key) - ) as t.ObjectProperty[] - ) - .map(prop => (prop.key as t.Identifier).name) - .includes(idName); - } - if (this.t.isRestElement(param)) return checkParam(param.argument); - - return false; - }; - - while (reversePath) { - const node = reversePath.node; - if (this.t.isArrowFunctionExpression(node) || this.t.isFunctionDeclaration(node)) { - for (const param of node.params) { - if (checkParam(param)) return true; - } - } - reversePath = reversePath.parentPath; - } - - return false; - } -} +import { + type CompParticle, + type DependencyProp, + DepMaskMap, + type ContextParticle, + type ExpParticle, + type ForParticle, + type HTMLParticle, + type IfParticle, + type MutableParticle, + type ReactivityParserConfig, + type TemplateParticle, + type TemplateProp, + type TextParticle, + type ViewParticle, +} from './types'; +import { type NodePath, type traverse, type types as t } from '@babel/core'; +import { + type CompUnit, + type ContextUnit, + type ExpUnit, + type ForUnit, + type HTMLUnit, + type IfUnit, + type TextUnit, + type UnitProp, + type ViewUnit, +} from '@openinula/jsx-view-parser'; +import { DLError } from './error'; +import { Dependency, getDependenciesFromNode } from './getDependencies'; + +export class ReactivityParser { + private readonly config: ReactivityParserConfig; + + private readonly t: typeof t; + private readonly traverse: typeof traverse; + private readonly depMaskMap: DepMaskMap; + private readonly reactivityFuncNames; + + private static readonly customHTMLProps = [ + 'didUpdate', + 'willMount', + 'didMount', + 'willUnmount', + 'didUnmount', + 'element', + 'innerHTML', + 'props', + 'attrs', + 'dataset', + 'forwardProps', + ]; + + readonly usedProperties = new Set(); + usedBit = 0; + + /** + * @brief Constructor + * @param config + */ + constructor(config: ReactivityParserConfig) { + this.config = config; + this.t = config.babelApi.types; + this.traverse = config.babelApi.traverse; + this.depMaskMap = config.depMaskMap; + this.reactivityFuncNames = config.reactivityFuncNames ?? []; + } + + /** + * @brief Parse the ViewUnit into a ViewParticle + * @returns + */ + parse(viewUnit: ViewUnit): ViewParticle { + return this.parseViewUnit(viewUnit); + } + + /** + * @brief Parse a ViewUnit into a ViewParticle + * @param viewUnit + * @returns ViewParticle + */ + private parseViewUnit(viewUnit: ViewUnit): ViewParticle { + if (this.isHTMLTemplate(viewUnit)) return this.parseTemplate(viewUnit as HTMLUnit); + if (viewUnit.type === 'text') return this.parseText(viewUnit); + if (viewUnit.type === 'html') return this.parseHTML(viewUnit); + if (viewUnit.type === 'comp') return this.parseComp(viewUnit); + if (viewUnit.type === 'for') return this.parseFor(viewUnit); + if (viewUnit.type === 'if') return this.parseIf(viewUnit); + if (viewUnit.type === 'context') return this.parseContext(viewUnit); + if (viewUnit.type === 'exp') return this.parseExp(viewUnit); + return DLError.throw1(); + } + + // ---- Parsers ---- + // ---- @Template ---- + /** + * @brief Collect static HTMLUnit into a template particle and generate a template string + * MutableParticle means whatever unit that is not a static HTMLUnit or a TextUnit + * Props means all the non-static props of the nested HTMLUnit or TextUnit, e.g. div().className(this.name) + * @param htmlUnit + * @returns TemplateParticle + */ + private parseTemplate(htmlUnit: HTMLUnit): TemplateParticle { + return { + type: 'template', + template: this.generateTemplate(htmlUnit), + props: this.parseTemplateProps(htmlUnit), + mutableParticles: this.generateMutableParticles(htmlUnit), + }; + } + + /** + * @brief Generate a template + * There'll be a situation where the tag is dynamic, e.g. tag(this.htmlTag), + * which we can't generate a template string for it, so we'll wrap it in an ExpParticle in parseHTML() section + * @returns template string + * @param unit + */ + private generateTemplate(unit: HTMLUnit): HTMLParticle { + const staticProps = this.filterTemplateProps( + // ---- Get all the static props + Object.entries(unit.props).filter( + ([, prop]) => + this.isStaticProp(prop) && + // ---- Filter out props with false values + !(this.t.isBooleanLiteral(prop.value) && !prop.value.value) + ) + ).map(([key, prop]) => [ + key, + { + ...prop, + depMask: 0, + allDepBits: [], + dependenciesNode: this.t.arrayExpression([]), + }, + ]); + + let children: ViewParticle[] = []; + if (!unit.props.textContent) { + children = unit.children + .map(unit => { + if (unit.type === 'html' && this.t.isStringLiteral(unit.tag)) { + return this.generateTemplate(unit); + } + if (unit.type === 'text' && this.t.isStringLiteral(unit.content)) { + return this.parseText(unit); + } + }) + .filter(Boolean) as HTMLParticle[]; + } + return { + type: 'html', + tag: unit.tag, + props: Object.fromEntries(staticProps), + children, + }; + } + + /** + * @brief Collect all the mutable nodes in a static HTMLUnit + * We use this function to collect mutable nodes' path and props, + * so that in the generate, we know which position to insert the mutable nodes + * @param htmlUnit + * @returns mutable particles + */ + private generateMutableParticles(htmlUnit: HTMLUnit): MutableParticle[] { + const mutableParticles: MutableParticle[] = []; + const generateMutableUnit = (unit: HTMLUnit, path: number[] = []) => { + // ---- Generate mutable particles for current HTMLUnit + unit.children?.forEach((child, idx) => { + if ( + !(child.type === 'html' && this.t.isStringLiteral(child.tag)) && + !(child.type === 'text' && this.t.isStringLiteral(child.content)) + ) { + mutableParticles.push({ + path: [...path, idx], + ...this.parseViewParticle(child), + }); + } + }); + // ---- Recursively generate mutable particles for static HTMLUnit children + unit.children + ?.filter( + child => + (child.type === 'html' && this.t.isStringLiteral(child.tag)) || + (child.type === 'text' && this.t.isStringLiteral(child.content)) + ) + .forEach((child, idx) => { + if (child.type === 'html') { + generateMutableUnit(child as HTMLUnit, [...path, idx]); + } + }); + }; + generateMutableUnit(htmlUnit); + + return mutableParticles; + } + + /** + * @brief Collect all the props in a static HTMLUnit or its nested HTMLUnit or TextUnit children + * Just like the mutable nodes, props are also equipped with path, + * so that we know which HTML ChildNode to insert the props + * @param htmlUnit + * @returns props + */ + private parseTemplateProps(htmlUnit: HTMLUnit): TemplateProp[] { + const templateProps: TemplateProp[] = []; + const generateVariableProp = (unit: HTMLUnit, path: number[]) => { + // ---- Generate all non-static(string/number/boolean) props for current HTMLUnit + // to be inserted further in the generate + Object.entries(unit.props) + .filter(([, prop]) => !this.isStaticProp(prop)) + .forEach(([key, prop]) => { + templateProps.push({ + tag: (unit.tag as t.StringLiteral).value, + key, + path, + value: prop.value, + ...this.getDependencies(prop.value), + }); + }); + // ---- Recursively generate props for static HTMLUnit children + unit.children + .filter( + child => + (child.type === 'html' && this.t.isStringLiteral(child.tag)) || + (child.type === 'text' && this.t.isStringLiteral(child.content)) + ) + .forEach((child, idx) => { + if (child.type === 'html') { + generateVariableProp(child, [...path, idx]); + } else if (child.type === 'text') { + // ---- if the child is a TextUnit, we just insert the text content + templateProps.push({ + tag: 'text', + key: 'value', + path: [...path, idx], + value: child.content, + depMask: 0, + dependenciesNode: this.t.arrayExpression([]), + allDepBits: [], + }); + } + }); + }; + generateVariableProp(htmlUnit, []); + + return templateProps; + } + + // ---- @Text ---- + /** + * @brief Parse a TextUnit into a TextParticle. + * This is only for a top level TextUnit, because if nested in HTMLUnit, it'll be parsed in the template string + * @param textUnit + * @returns TextParticle + */ + private parseText(textUnit: TextUnit): TextParticle { + return { + type: 'text', + content: { + value: textUnit.content, + ...this.getDependencies(textUnit.content), + }, + }; + } + + // ---- @HTML ---- + /** + * @brief Parse an HTMLUnit with a dynamic tag into an ExpParticle or an HTMLParticle + * We detect dependencies in the tag, if there's no dependency, + * we parse it as an HTMLParticle and dynamically append it to the parent node; + * if there's dependency, we parse it as an ExpParticle and wrap it in an ExpParticle + * so that we can make the tag reactive + * @param htmlUnit + * @returns ExpParticle | HTMLParticle + */ + private parseHTML(htmlUnit: HTMLUnit): ExpParticle | HTMLParticle { + const innerHTMLParticle: HTMLParticle = { + type: 'html', + tag: htmlUnit.tag, + props: {}, + children: [], + }; + + innerHTMLParticle.props = Object.fromEntries( + Object.entries(htmlUnit.props).map(([key, prop]) => [key, this.generateDependencyProp(prop)]) + ); + + innerHTMLParticle.children = htmlUnit.children.map(this.parseViewParticle.bind(this)); + + // ---- Not a dynamic tag + return innerHTMLParticle; + } + + // ---- @Comp ---- + /** + * @brief Parse a CompUnit into a CompParticle or an ExpParticle + * Similar to parseHTML(), we detect dependencies in the tag, if there's no dependency, + * we parse it as a regular CompParticle, otherwise we wrap it with an ExpParticle. + * @param compUnit + * @returns CompParticle | ExpParticle + */ + private parseComp(compUnit: CompUnit): CompParticle { + const compParticle: CompParticle = { + type: 'comp', + tag: compUnit.tag, + props: {}, + children: [], + }; + + compParticle.props = Object.fromEntries( + Object.entries(compUnit.props).map(([key, prop]) => [key, this.generateDependencyProp(prop)]) + ); + compParticle.children = compUnit.children.map(this.parseViewParticle.bind(this)); + + return compParticle; + } + + // ---- @For ---- + /** + * @brief Parse a ForUnit into a ForParticle with dependencies + * Key and item doesn't need to be reactive, so here we don't collect dependencies for it + * @param forUnit + * @returns ForParticle + */ + private parseFor(forUnit: ForUnit): ForParticle { + const { dependenciesNode, allDepBits } = this.getDependencies(forUnit.array); + const prevMap = this.config.depMaskMap; + + // ---- Generate an identifierDepMap to track identifiers in item and make them reactive + // based on the dependencies from the array + // Just wrap the item in an assignment expression to get all the identifiers + const itemWrapper = this.t.assignmentExpression('=', forUnit.item, this.t.objectExpression([])); + this.config.depMaskMap = new Map([ + ...this.config.depMaskMap, + ...this.getIdentifiers(itemWrapper).map(id => [id, allDepBits] as const), + ]); + + const forParticle: ForParticle = { + type: 'for', + item: forUnit.item, + index: forUnit.index, + array: { + value: forUnit.array, + allDepBits, + dependenciesNode, + }, + children: forUnit.children.map(this.parseViewParticle.bind(this)), + key: forUnit.key, + }; + this.config.depMaskMap = prevMap; + return forParticle; + } + + // ---- @If ---- + /** + * @brief Parse an IfUnit into an IfParticle with dependencies + * @param ifUnit + * @returns IfParticle + */ + private parseIf(ifUnit: IfUnit): IfParticle { + return { + type: 'if', + branches: ifUnit.branches.map(branch => ({ + condition: { + value: branch.condition, + ...this.getDependencies(branch.condition), + }, + children: branch.children.map(this.parseViewParticle.bind(this)), + })), + }; + } + + // ---- @Env ---- + /** + * @brief Parse an EnvUnit into an EnvParticle with dependencies + * @param contextProviderUnit + * @returns ContextParticle + */ + private parseContext(contextProviderUnit: ContextUnit): ContextParticle { + return { + type: 'context', + contextName: contextProviderUnit.contextName, + props: Object.fromEntries( + Object.entries(contextProviderUnit.props).map(([key, prop]) => [key, this.generateDependencyProp(prop)]) + ), + children: contextProviderUnit.children.map(this.parseViewParticle.bind(this)), + }; + } + + // ---- @Exp ---- + /** + * @brief Parse an ExpUnit into an ExpParticle with dependencies + * @param expUnit + * @returns ExpParticle + */ + private parseExp(expUnit: ExpUnit): ExpParticle { + return { + type: 'exp', + content: this.generateDependencyProp(expUnit.content), + }; + } + + // ---- Dependencies ---- + /** + * @brief Generate a dependency prop with dependencies + * @param prop + * @returns DependencyProp + */ + private generateDependencyProp(prop: UnitProp): DependencyProp { + return { + value: prop.value, + ...this.getDependencies(prop.value), + viewPropMap: Object.fromEntries( + Object.entries(prop.viewPropMap).map(([key, units]) => [key, units.map(this.parseViewParticle.bind(this))]) + ), + }; + } + + /** + * @brief Get all the dependencies of a node + * this.dependencyParseType controls how we parse the dependencies + * 1. property: parse the dependencies of a node as a property, e.g. this.name + * 2. identifier: parse the dependencies of a node as an identifier, e.g. name + * The availableProperties is the list of all the properties that can be used in the template, + * no matter it's a property or an identifier + * @param node + * @returns dependency index array + */ + private getDependencies(node: t.Expression | t.Statement): Dependency { + if (this.t.isFunctionExpression(node) || this.t.isArrowFunctionExpression(node)) { + return { + allDepBits: [], + dependenciesNode: this.t.arrayExpression([]), + }; + } + // ---- Both id and prop deps need to be calculated because + // id is for snippet update, prop is normal update + // in a snippet, the depsNode should be both id and prop + const dependency = getDependenciesFromNode(node, this.depMaskMap, this.reactivityFuncNames); + this.usedBit |= dependency.allDepBits.reduce((acc, cur) => acc | cur, 0); + return dependency; + } + + // ---- Utils ---- + /** + * @brief Parse a ViewUnit into a ViewParticle by new-ing a ReactivityParser + * @param viewUnit + * @returns ViewParticle + */ + private parseViewParticle(viewUnit: ViewUnit): ViewParticle { + const parser = new ReactivityParser(this.config); + const parsedUnit = parser.parse(viewUnit); + // ---- Collect used properties + parser.usedProperties.forEach(this.usedProperties.add.bind(this.usedProperties)); + this.usedBit |= parser.usedBit; + return parsedUnit; + } + + /** + * @brief Check if a ViewUnit is a static HTMLUnit that can be parsed into a template + * Must satisfy: + * 1. type is html + * 2. tag is a string literal, i.e., non-dynamic tag + * 3. has at least one child that is a static HTMLUnit, + * or else just call a createElement function, no need for template clone + * @param viewUnit + * @returns is a static HTMLUnit + */ + private isHTMLTemplate(viewUnit: ViewUnit): boolean { + return ( + viewUnit.type === 'html' && + this.t.isStringLiteral(viewUnit.tag) && + !!viewUnit.children?.some(child => child.type === 'html' && this.t.isStringLiteral(child.tag)) + ); + } + + /** + * @brief Check if a prop is a static prop + * i.e. + * 1. no viewPropMap + * 2. value is a string/number/boolean literal + * @param prop + * @returns is a static prop + */ + private isStaticProp(prop: UnitProp): boolean { + const { value, viewPropMap } = prop; + return ( + (!viewPropMap || Object.keys(viewPropMap).length === 0) && + (this.t.isStringLiteral(value) || this.t.isNumericLiteral(value) || this.t.isBooleanLiteral(value)) + ); + } + + /** + * @brief Filter out some props that are not needed in the template, + * these are all special props to be parsed differently in the generate + * @param props + * @returns filtered props + */ + private filterTemplateProps(props: Array<[string, T]>): Array<[string, T]> { + return ( + props + // ---- Filter out event listeners + .filter(([key]) => !key.startsWith('on')) + // ---- Filter out specific props + .filter(([key]) => !ReactivityParser.customHTMLProps.includes(key)) + ); + } + + /** + * @brief Wrap the value in a file + * @param node + * @returns wrapped value + */ + private valueWrapper(node: t.Expression | t.Statement): t.File { + return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)])); + } + + /** + * @brief Check if an identifier is a simple stand alone identifier, + * i.e., not a member expression, nor a function param + * @param path + * 1. not a member expression + * 2. not a function param + * 3. not in a declaration + * 4. not as object property's not computed key + * @returns is a stand alone identifier + */ + private isStandAloneIdentifier(path: NodePath): boolean { + const node = path.node; + const parentNode = path.parentPath?.node; + const isMemberExpression = this.t.isMemberExpression(parentNode) && parentNode.property === node; + if (isMemberExpression) return false; + const isFunctionParam = this.isAttrFromFunction(path, node.name); + if (isFunctionParam) return false; + while (path.parentPath) { + if (this.t.isVariableDeclarator(path.parentPath.node)) return false; + if ( + this.t.isObjectProperty(path.parentPath.node) && + path.parentPath.node.key === path.node && + !path.parentPath.node.computed + ) + return false; + path = path.parentPath as NodePath; + } + return true; + } + + /** + * @brief Get all identifiers as strings in a node + * @param node + * @returns identifiers + */ + private getIdentifiers(node: t.Node): string[] { + if (this.t.isIdentifier(node)) return [node.name]; + const identifierKeys = new Set(); + this.traverse(this.valueWrapper(node as t.Expression), { + Identifier: innerPath => { + if (!this.isStandAloneIdentifier(innerPath)) return; + identifierKeys.add(innerPath.node.name); + }, + }); + return [...identifierKeys]; + } + + /** + * @brief check if the identifier is from a function param till the stopNode + * e.g: + * function myFunc1(ok) { // stopNode = functionBody + * const myFunc2 = ok => ok // from function param + * console.log(ok) // not from function param + * } + */ + private isAttrFromFunction(path: NodePath, idName: string) { + let reversePath = path.parentPath; + + const checkParam: (param: t.Node) => boolean = (param: t.Node) => { + // ---- 3 general types: + // * represent allow nesting + // ---0 Identifier: (a) + // ---1 RestElement: (...a) * + // ---1 Pattern: 3 sub Pattern + // -----0 AssignmentPattern: (a=1) * + // -----1 ArrayPattern: ([a, b]) * + // -----2 ObjectPattern: ({a, b}) + if (this.t.isIdentifier(param)) return param.name === idName; + if (this.t.isAssignmentPattern(param)) return checkParam(param.left); + if (this.t.isArrayPattern(param)) { + return param.elements + .filter(Boolean) + .map(el => checkParam(el!)) + .includes(true); + } + if (this.t.isObjectPattern(param)) { + return ( + param.properties.filter( + prop => this.t.isObjectProperty(prop) && this.t.isIdentifier(prop.key) + ) as t.ObjectProperty[] + ) + .map(prop => (prop.key as t.Identifier).name) + .includes(idName); + } + if (this.t.isRestElement(param)) return checkParam(param.argument); + + return false; + }; + + while (reversePath) { + const node = reversePath.node; + if (this.t.isArrowFunctionExpression(node) || this.t.isFunctionDeclaration(node)) { + for (const param of node.params) { + if (checkParam(param)) return true; + } + } + reversePath = reversePath.parentPath; + } + + return false; + } +} diff --git a/packages/transpiler/reactivity-parser/src/types.ts b/packages/transpiler/reactivity-parser/src/types.ts index 6435166168c2efde75349779f1f29449bc5125b1..8b9e51d571ff21b0a0d72b2728f46d9ba6afb27e 100644 --- a/packages/transpiler/reactivity-parser/src/types.ts +++ b/packages/transpiler/reactivity-parser/src/types.ts @@ -1,109 +1,109 @@ -import { type types as t } from '@babel/core'; -import type Babel from '@babel/core'; - -export interface DependencyValue { - value: T; - depMask?: number; // -> bit - allDepBits: number[]; - dependenciesNode: t.ArrayExpression; -} - -export interface DependencyProp { - value: t.Expression; - viewPropMap: Record; - depMask?: number; - allDepBits: number[]; - dependenciesNode: t.ArrayExpression; -} - -export interface TemplateProp { - tag: string; - key: string; - path: number[]; - value: t.Expression; - depMask?: number; - allDepBits: number[]; - dependenciesNode: t.ArrayExpression; -} - -export type MutableParticle = ViewParticle & { path: number[] }; - -export interface TemplateParticle { - type: 'template'; - template: HTMLParticle; - mutableParticles: MutableParticle[]; - props: TemplateProp[]; -} - -export interface TextParticle { - type: 'text'; - content: DependencyValue; -} - -export interface HTMLParticle { - type: 'html'; - tag: t.Expression; - props: Record>; - children: ViewParticle[]; -} - -export interface CompParticle { - type: 'comp'; - tag: t.Expression; - props: Record; - children: ViewParticle[]; -} - -export interface ForParticle { - type: 'for'; - item: t.LVal; - index: t.Identifier | null; - array: DependencyValue; - key: t.Expression; - children: ViewParticle[]; -} - -export interface IfBranch { - condition: DependencyValue; - children: ViewParticle[]; -} - -export interface IfParticle { - type: 'if'; - branches: IfBranch[]; -} - -export interface ContextParticle { - type: 'context'; - props: Record; - children: ViewParticle[]; - contextName: string; -} - -export interface ExpParticle { - type: 'exp'; - content: DependencyProp; -} - -export type ViewParticle = - | TemplateParticle - | TextParticle - | HTMLParticle - | CompParticle - | ForParticle - | IfParticle - | ContextParticle - | ExpParticle; - -export interface ReactivityParserConfig { - babelApi: typeof Babel; - depMaskMap: DepMaskMap; - identifierDepMap?: Record; - dependencyParseType?: 'property' | 'identifier'; - parseTemplate?: boolean; - reactivityFuncNames?: string[]; -} - -// TODO: unify with the types in babel-inula-next-core -export type Bitmap = number; -export type DepMaskMap = Map; +import { type types as t } from '@babel/core'; +import type Babel from '@babel/core'; + +export interface DependencyValue { + value: T; + depMask?: number; // -> bit + allDepBits: number[]; + dependenciesNode: t.ArrayExpression; +} + +export interface DependencyProp { + value: t.Expression; + viewPropMap: Record; + depMask?: number; + allDepBits: number[]; + dependenciesNode: t.ArrayExpression; +} + +export interface TemplateProp { + tag: string; + key: string; + path: number[]; + value: t.Expression; + depMask?: number; + allDepBits: number[]; + dependenciesNode: t.ArrayExpression; +} + +export type MutableParticle = ViewParticle & { path: number[] }; + +export interface TemplateParticle { + type: 'template'; + template: HTMLParticle; + mutableParticles: MutableParticle[]; + props: TemplateProp[]; +} + +export interface TextParticle { + type: 'text'; + content: DependencyValue; +} + +export interface HTMLParticle { + type: 'html'; + tag: t.Expression; + props: Record>; + children: ViewParticle[]; +} + +export interface CompParticle { + type: 'comp'; + tag: t.Expression; + props: Record; + children: ViewParticle[]; +} + +export interface ForParticle { + type: 'for'; + item: t.LVal; + index: t.Identifier | null; + array: DependencyValue; + key: t.Expression; + children: ViewParticle[]; +} + +export interface IfBranch { + condition: DependencyValue; + children: ViewParticle[]; +} + +export interface IfParticle { + type: 'if'; + branches: IfBranch[]; +} + +export interface ContextParticle { + type: 'context'; + props: Record; + children: ViewParticle[]; + contextName: string; +} + +export interface ExpParticle { + type: 'exp'; + content: DependencyProp; +} + +export type ViewParticle = + | TemplateParticle + | TextParticle + | HTMLParticle + | CompParticle + | ForParticle + | IfParticle + | ContextParticle + | ExpParticle; + +export interface ReactivityParserConfig { + babelApi: typeof Babel; + depMaskMap: DepMaskMap; + identifierDepMap?: Record; + dependencyParseType?: 'property' | 'identifier'; + parseTemplate?: boolean; + reactivityFuncNames?: string[]; +} + +// TODO: unify with the types in babel-inula-next-core +export type Bitmap = number; +export type DepMaskMap = Map; diff --git a/packages/transpiler/reactivity-parser/tsconfig.json b/packages/transpiler/reactivity-parser/tsconfig.json index e0932d7828a8e6b8f5b2c7752a24057e6461adf0..c003315788d1e0451295fc2f3330f040cbeb5b00 100644 --- a/packages/transpiler/reactivity-parser/tsconfig.json +++ b/packages/transpiler/reactivity-parser/tsconfig.json @@ -1,13 +1,13 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", - "strict": true, - "esModuleInterop": true - }, - "ts-node": { - "esm": true - } +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true + }, + "ts-node": { + "esm": true + } } \ No newline at end of file diff --git a/packages/transpiler/tranpiler-dev/CHANGELOG.md b/packages/transpiler/tranpiler-dev/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..f24522df1febe327e2e974586ce123a73c3246c3 --- /dev/null +++ b/packages/transpiler/tranpiler-dev/CHANGELOG.md @@ -0,0 +1,8 @@ +# tranpiler-dev + +## 0.0.1 + +### Patch Changes + +- Updated dependencies + - babel-preset-inula-next@0.0.4 diff --git a/packages/transpiler/tranpiler-dev/index.html b/packages/transpiler/tranpiler-dev/index.html index 3d08438199d14c4d755e2e2a32c2e02752c7635f..0ff5f4fd7d4312660290a4b2ec16565ce499105a 100644 --- a/packages/transpiler/tranpiler-dev/index.html +++ b/packages/transpiler/tranpiler-dev/index.html @@ -1,13 +1,13 @@ - - - - - - - Vite App - - -
    - - - + + + + + + + Vite App + + +
    + + + diff --git a/packages/transpiler/tranpiler-dev/main.js b/packages/transpiler/tranpiler-dev/main.js index 2f242f438584a06bba070eebdfe253216a35b905..01757040441cf4a9da4856b77716111a629fcce1 100644 --- a/packages/transpiler/tranpiler-dev/main.js +++ b/packages/transpiler/tranpiler-dev/main.js @@ -1,19 +1,19 @@ -import { transform } from '@babel/standalone'; -import preset from '@openinula/babel-preset-inula-next'; - -const code = /*js*/ ` -console.log('ok'); - -const Comp = Component(() => { - const prop1_$p$_ = 1 - const prop2_$p$_ = 1 - - return <> -}) -`; - -const result = transform(code, { - presets: [preset], -}).code; - -console.log(result); +import { transform } from '@babel/standalone'; +import preset from '@openinula/babel-preset-inula-next'; + +const code = /*js*/ ` +console.log('ok'); + +const Comp = Component(() => { + const prop1_$p$_ = 1 + const prop2_$p$_ = 1 + + return <> +}) +`; + +const result = transform(code, { + presets: [preset], +}).code; + +console.log(result); diff --git a/packages/transpiler/view-generator/package.json b/packages/transpiler/view-generator/package.json index c51e625a10f8e10199ac9716606ce79c75e943fd..38a6e1395fd097111c2863efecd042f12b423008 100644 --- a/packages/transpiler/view-generator/package.json +++ b/packages/transpiler/view-generator/package.json @@ -1,42 +1,44 @@ -{ - "name": "@openinula/view-generator", - "version": "0.0.2", - "description": "InulaNext View Generator given different types of reactivity units", - "keywords": [ - "Inula-Next" - ], - "license": "MIT", - "files": [ - "dist" - ], - "type": "module", - "main": "dist/index.js", - "module": "dist/index.js", - "typings": "dist/index.d.ts", - "scripts": { - "build": "tsup --sourcemap" - }, - "devDependencies": { - "@babel/core": "^7.20.12", - "@types/babel__core": "^7.20.5", - "@types/node": "^20.10.5", - "tsup": "^6.7.0", - "typescript": "^5.3.2", - "@openinula/reactivity-parser": "workspace:*" - }, - "dependencies": { - "@openinula/error-handler": "workspace:*" - }, - "tsup": { - "entry": [ - "src/index.ts" - ], - "format": [ - "cjs", - "esm" - ], - "clean": true, - "dts": true, - "sourceMap": true - } -} +{ + "name": "@openinula/view-generator", + "version": "0.0.2", + "description": "InulaNext View Generator given different types of reactivity units", + "keywords": [ + "Inula-Next" + ], + "license": "MIT", + "files": [ + "dist" + ], + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "typings": "dist/index.d.ts", + "scripts": { + "build": "tsup --sourcemap" + }, + "devDependencies": { + "@babel/core": "^7.20.12", + "@types/babel__core": "^7.20.5", + "@types/node": "^20.10.5", + "tsup": "^6.7.0", + "typescript": "^5.3.2", + "@openinula/reactivity-parser": "workspace:*" + }, + "dependencies": { + "@openinula/error-handler": "workspace:*", + "@openinula/next-shared": "workspace:*", + "@openinula/babel-api": "workspace:*" + }, + "tsup": { + "entry": [ + "src/index.ts" + ], + "format": [ + "cjs", + "esm" + ], + "clean": true, + "dts": true, + "sourceMap": true + } +} diff --git a/packages/transpiler/view-generator/src/HelperGenerators/BaseGenerator.ts b/packages/transpiler/view-generator/src/HelperGenerators/BaseGenerator.ts index cf27ac1c1894b4a8597cfd540d6e4768b0c82e22..73aeb17461d18ff0a1e332396d215afa895707b5 100644 --- a/packages/transpiler/view-generator/src/HelperGenerators/BaseGenerator.ts +++ b/packages/transpiler/view-generator/src/HelperGenerators/BaseGenerator.ts @@ -1,263 +1,263 @@ -import { type types as t, type traverse } from '@babel/core'; -import { Bitmap, type ViewParticle } from '@openinula/reactivity-parser'; -import { type ViewGeneratorConfig } from '../types'; -import ViewGenerator from '../ViewGenerator'; - -export const prefixMap = { template: '$t', node: '$node' }; - -export default class BaseGenerator { - readonly viewParticle: ViewParticle; - readonly config: ViewGeneratorConfig; - - readonly t: typeof t; - readonly traverse: typeof traverse; - readonly importMap: Record; - readonly elementAttributeMap; - readonly alterAttributeMap; - - readonly viewGenerator; - - /** - * @brief Constructor - * @param viewParticle - * @param config - */ - constructor(viewParticle: ViewParticle, config: ViewGeneratorConfig) { - this.viewParticle = viewParticle; - this.config = config; - this.t = config.babelApi.types; - this.traverse = config.babelApi.traverse; - this.importMap = config.importMap; - this.viewGenerator = new ViewGenerator(config); - this.elementAttributeMap = config.attributeMap - ? Object.entries(config.attributeMap).reduce>((acc, [key, elements]) => { - elements.forEach(element => { - if (!acc[element]) acc[element] = []; - acc[element].push(key); - }); - return acc; - }, {}) - : {}; - this.alterAttributeMap = config.alterAttributeMap; - } - - // ---- Init Statements - private readonly initStatements: t.Statement[] = []; - - addInitStatement(...statements: (t.Statement | null)[]) { - this.initStatements.push(...(statements.filter(Boolean) as t.Statement[])); - } - - // ---- Added Class Properties, typically used in for Template - private readonly templates: t.VariableDeclaration[] = []; - - addTemplate(key: string, value: t.Expression) { - this.templates.push( - this.t.variableDeclaration('const', [this.t.variableDeclarator(this.t.identifier(key), value)]) - ); - } - - // ---- Update Statements - private readonly updateStatements: Record = {}; - - addUpdateStatements(depMask: Bitmap | undefined, statement: t.Statement | undefined | null) { - if (!depMask) return; - if (!this.updateStatements[depMask]) this.updateStatements[depMask] = []; - if (statement) this.updateStatements[depMask].push(statement); - } - - addUpdateStatementsWithoutDep(statement: t.Statement) { - if (!this.updateStatements[0]) this.updateStatements[0] = []; - this.updateStatements[0].push(statement); - } - - /** - * @returns [initStatements, updateStatements, classProperties, nodeName] - */ - generate(): [t.Statement[], Record, t.VariableDeclaration[], string] { - const nodeName = this.run(); - return [this.initStatements, this.updateStatements, this.templates, nodeName]; - } - - /** - * @brief Generate the view given the view particles, mainly used for child particles parsing - * @param viewParticles - * @param mergeStatements - * @returns [initStatements, topLevelNodes, updateStatements] - */ - generateChildren( - viewParticles: ViewParticle[], - mergeStatements = true, - newIdx = false - ): [t.Statement[], string[], Record, number] { - this.viewGenerator.nodeIdx = newIdx ? -1 : this.nodeIdx; - this.viewGenerator.templateIdx = this.templateIdx; - const [initStatements, updateStatements, templates, topLevelNodes] = - this.viewGenerator.generateChildren(viewParticles); - if (!newIdx) this.nodeIdx = this.viewGenerator.nodeIdx; - this.templateIdx = this.viewGenerator.templateIdx; - this.templates.push(...templates); - if (mergeStatements) this.mergeStatements(updateStatements); - - return [initStatements, topLevelNodes, updateStatements, this.viewGenerator.nodeIdx]; - } - - /** - * @brief Merge the update statements - * @param statements - */ - private mergeStatements(statements: Record): void { - Object.entries(statements).forEach(([depNum, statements]) => { - if (!this.updateStatements[Number(depNum)]) { - this.updateStatements[Number(depNum)] = []; - } - this.updateStatements[Number(depNum)].push(...statements); - }); - } - - /** - * @brief Generate the view given the view particle - * @param viewParticle - * @param mergeStatements - * @returns [initStatements, nodeName, updateStatements] - */ - generateChild( - viewParticle: ViewParticle, - mergeStatements = true, - newIdx = false - ): [t.Statement[], string, Record, number] { - this.viewGenerator.nodeIdx = newIdx ? -1 : this.nodeIdx; - this.viewGenerator.templateIdx = this.templateIdx; - const [initStatements, updateStatements, templates, nodeName] = this.viewGenerator.generateChild(viewParticle); - if (!newIdx) this.nodeIdx = this.viewGenerator.nodeIdx; - this.templateIdx = this.viewGenerator.templateIdx; - this.templates.push(...templates); - if (mergeStatements) this.mergeStatements(updateStatements); - - return [initStatements, nodeName, updateStatements, this.viewGenerator.nodeIdx]; - } - - /** - * @View - * const $update = (changed) => { ${updateStatements} } - */ - geneUpdateFunc(updateStatements: Record): t.Statement { - return this.t.variableDeclaration('const', [ - this.t.variableDeclarator( - this.t.identifier('$update'), - this.t.arrowFunctionExpression([this.t.identifier('$changed')], this.geneUpdateBody(updateStatements)) - ), - ]); - } - - get updateParams() { - return [this.t.identifier('$changed')]; - } - - /** - * @View - * (changed) => { - * if (changed & 1) { - * ... - * } - * ... - * } - */ - geneUpdateBody(updateStatements: Record): t.BlockStatement { - return this.t.blockStatement([ - ...Object.entries(updateStatements) - .filter(([depNum]) => depNum !== '0') - .map(([depNum, statements]) => { - return this.t.ifStatement( - this.t.binaryExpression('&', this.t.identifier('$changed'), this.t.numericLiteral(Number(depNum))), - this.t.blockStatement(statements) - ); - }), - ...(updateStatements[0] ?? []), - ]); - } - - /** - * @View - * let node1, node2, ... - */ - declareNodes(nodeIdx: number): t.VariableDeclaration[] { - if (nodeIdx === -1) return []; - return [ - this.t.variableDeclaration( - 'let', - Array.from({ length: nodeIdx + 1 }, (_, i) => - this.t.variableDeclarator(this.t.identifier(`${prefixMap.node}${i}`)) - ) - ), - ]; - } - - /** - * @View - * return [${topLevelNodes}] - */ - generateReturnStatement(topLevelNodes: string[]): t.ReturnStatement { - return this.t.returnStatement(this.t.arrayExpression(topLevelNodes.map(name => this.t.identifier(name)))); - } - - /** - * @brief To be implemented by the subclass as the main node generation function - * @returns dlNodeName - */ - run(): string { - return ''; - } - - // ---- Name ---- - // ---- Used as dlNodeName for any node declaration - nodeIdx = -1; - - generateNodeName(idx?: number): string { - return `${prefixMap.node}${idx ?? ++this.nodeIdx}`; - } - - // ---- Used as template generation as class property - templateIdx = -1; - - generateTemplateName(): string { - return this.config.genTemplateKey(`${prefixMap.template}${++this.templateIdx}`); - } - - // ---- @Utils ----- - /** - * @brief Wrap the value in a file - * @param node - * @returns wrapped value - */ - valueWrapper(node: t.Expression | t.Statement): t.File { - return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)])); - } - - /** - * @View - * ${dlNodeName} && ${expression} - */ - optionalExpression(dlNodeName: string, expression: t.Expression): t.Statement { - return this.t.expressionStatement(this.t.logicalExpression('&&', this.t.identifier(dlNodeName), expression)); - } - - /** - * @brief Shorthand function for collecting statements in batch - * @returns [statements, collect] - */ - static statementsCollector(): [t.Statement[], (...statements: t.Statement[] | t.Statement[][]) => void] { - const statements: t.Statement[] = []; - const collect = (...newStatements: t.Statement[] | t.Statement[][]) => { - newStatements.forEach(s => { - if (Array.isArray(s)) { - statements.push(...s); - } else { - statements.push(s); - } - }); - }; - - return [statements, collect]; - } -} +import { type types as t, type traverse } from '@babel/core'; +import { Bitmap, type ViewParticle } from '@openinula/reactivity-parser'; +import { type ViewGeneratorConfig } from '../types'; +import ViewGenerator from '../ViewGenerator'; + +export const prefixMap = { template: '$t', node: '$node' }; + +export default class BaseGenerator { + readonly viewParticle: ViewParticle; + readonly config: ViewGeneratorConfig; + + readonly t: typeof t; + readonly traverse: typeof traverse; + readonly importMap: Record; + readonly elementAttributeMap; + readonly alterAttributeMap; + + readonly viewGenerator; + + /** + * @brief Constructor + * @param viewParticle + * @param config + */ + constructor(viewParticle: ViewParticle, config: ViewGeneratorConfig) { + this.viewParticle = viewParticle; + this.config = config; + this.t = config.babelApi.types; + this.traverse = config.babelApi.traverse; + this.importMap = config.importMap; + this.viewGenerator = new ViewGenerator(config); + this.elementAttributeMap = config.attributeMap + ? Object.entries(config.attributeMap).reduce>((acc, [key, elements]) => { + elements.forEach(element => { + if (!acc[element]) acc[element] = []; + acc[element].push(key); + }); + return acc; + }, {}) + : {}; + this.alterAttributeMap = config.alterAttributeMap; + } + + // ---- Init Statements + private readonly initStatements: t.Statement[] = []; + + addInitStatement(...statements: (t.Statement | null)[]) { + this.initStatements.push(...(statements.filter(Boolean) as t.Statement[])); + } + + // ---- Added Class Properties, typically used in for Template + private readonly templates: t.VariableDeclaration[] = []; + + addTemplate(key: string, value: t.Expression) { + this.templates.push( + this.t.variableDeclaration('const', [this.t.variableDeclarator(this.t.identifier(key), value)]) + ); + } + + // ---- Update Statements + private readonly updateStatements: Record = {}; + + addUpdateStatements(depMask: Bitmap | undefined, statement: t.Statement | undefined | null) { + if (!depMask) return; + if (!this.updateStatements[depMask]) this.updateStatements[depMask] = []; + if (statement) this.updateStatements[depMask].push(statement); + } + + addUpdateStatementsWithoutDep(statement: t.Statement) { + if (!this.updateStatements[0]) this.updateStatements[0] = []; + this.updateStatements[0].push(statement); + } + + /** + * @returns [initStatements, updateStatements, classProperties, nodeName] + */ + generate(): [t.Statement[], Record, t.VariableDeclaration[], string] { + const nodeName = this.run(); + return [this.initStatements, this.updateStatements, this.templates, nodeName]; + } + + /** + * @brief Generate the view given the view particles, mainly used for child particles parsing + * @param viewParticles + * @param mergeStatements + * @returns [initStatements, topLevelNodes, updateStatements] + */ + generateChildren( + viewParticles: ViewParticle[], + mergeStatements = true, + newIdx = false + ): [t.Statement[], string[], Record, number] { + this.viewGenerator.nodeIdx = newIdx ? -1 : this.nodeIdx; + this.viewGenerator.templateIdx = this.templateIdx; + const [initStatements, updateStatements, templates, topLevelNodes] = + this.viewGenerator.generateChildren(viewParticles); + if (!newIdx) this.nodeIdx = this.viewGenerator.nodeIdx; + this.templateIdx = this.viewGenerator.templateIdx; + this.templates.push(...templates); + if (mergeStatements) this.mergeStatements(updateStatements); + + return [initStatements, topLevelNodes, updateStatements, this.viewGenerator.nodeIdx]; + } + + /** + * @brief Merge the update statements + * @param statements + */ + private mergeStatements(statements: Record): void { + Object.entries(statements).forEach(([depNum, statements]) => { + if (!this.updateStatements[Number(depNum)]) { + this.updateStatements[Number(depNum)] = []; + } + this.updateStatements[Number(depNum)].push(...statements); + }); + } + + /** + * @brief Generate the view given the view particle + * @param viewParticle + * @param mergeStatements + * @returns [initStatements, nodeName, updateStatements] + */ + generateChild( + viewParticle: ViewParticle, + mergeStatements = true, + newIdx = false + ): [t.Statement[], string, Record, number] { + this.viewGenerator.nodeIdx = newIdx ? -1 : this.nodeIdx; + this.viewGenerator.templateIdx = this.templateIdx; + const [initStatements, updateStatements, templates, nodeName] = this.viewGenerator.generateChild(viewParticle); + if (!newIdx) this.nodeIdx = this.viewGenerator.nodeIdx; + this.templateIdx = this.viewGenerator.templateIdx; + this.templates.push(...templates); + if (mergeStatements) this.mergeStatements(updateStatements); + + return [initStatements, nodeName, updateStatements, this.viewGenerator.nodeIdx]; + } + + /** + * @View + * const $update = (changed) => { ${updateStatements} } + */ + geneUpdateFunc(updateStatements: Record): t.Statement { + return this.t.variableDeclaration('const', [ + this.t.variableDeclarator( + this.t.identifier('$update'), + this.t.arrowFunctionExpression([this.t.identifier('$changed')], this.geneUpdateBody(updateStatements)) + ), + ]); + } + + get updateParams() { + return [this.t.identifier('$changed')]; + } + + /** + * @View + * (changed) => { + * if (changed & 1) { + * ... + * } + * ... + * } + */ + geneUpdateBody(updateStatements: Record): t.BlockStatement { + return this.t.blockStatement([ + ...Object.entries(updateStatements) + .filter(([depNum]) => depNum !== '0') + .map(([depNum, statements]) => { + return this.t.ifStatement( + this.t.binaryExpression('&', this.t.identifier('$changed'), this.t.numericLiteral(Number(depNum))), + this.t.blockStatement(statements) + ); + }), + ...(updateStatements[0] ?? []), + ]); + } + + /** + * @View + * let node1, node2, ... + */ + declareNodes(nodeIdx: number): t.VariableDeclaration[] { + if (nodeIdx === -1) return []; + return [ + this.t.variableDeclaration( + 'let', + Array.from({ length: nodeIdx + 1 }, (_, i) => + this.t.variableDeclarator(this.t.identifier(`${prefixMap.node}${i}`)) + ) + ), + ]; + } + + /** + * @View + * return [${topLevelNodes}] + */ + generateReturnStatement(topLevelNodes: string[]): t.ReturnStatement { + return this.t.returnStatement(this.t.arrayExpression(topLevelNodes.map(name => this.t.identifier(name)))); + } + + /** + * @brief To be implemented by the subclass as the main node generation function + * @returns nodeName + */ + run(): string { + return ''; + } + + // ---- Name ---- + // ---- Used as nodeName for any node declaration + nodeIdx = -1; + + generateNodeName(idx?: number): string { + return `${prefixMap.node}${idx ?? ++this.nodeIdx}`; + } + + // ---- Used as template generation as class property + templateIdx = -1; + + generateTemplateName(): string { + return this.config.genTemplateKey(`${prefixMap.template}${++this.templateIdx}`); + } + + // ---- @Utils ----- + /** + * @brief Wrap the value in a file + * @param node + * @returns wrapped value + */ + valueWrapper(node: t.Expression | t.Statement): t.File { + return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)])); + } + + /** + * @View + * ${nodeName} && ${expression} + */ + optionalExpression(nodeName: string, expression: t.Expression): t.Statement { + return this.t.expressionStatement(this.t.logicalExpression('&&', this.t.identifier(nodeName), expression)); + } + + /** + * @brief Shorthand function for collecting statements in batch + * @returns [statements, collect] + */ + static statementsCollector(): [t.Statement[], (...statements: t.Statement[] | t.Statement[][]) => void] { + const statements: t.Statement[] = []; + const collect = (...newStatements: t.Statement[] | t.Statement[][]) => { + newStatements.forEach(s => { + if (Array.isArray(s)) { + statements.push(...s); + } else { + statements.push(s); + } + }); + }; + + return [statements, collect]; + } +} diff --git a/packages/transpiler/view-generator/src/HelperGenerators/CondGenerator.ts b/packages/transpiler/view-generator/src/HelperGenerators/CondGenerator.ts index 2a9cefff2187ebad290736b07079d4bda103da6b..ae51d6a4d184e19a6a598f383c191ad64289f9e7 100644 --- a/packages/transpiler/view-generator/src/HelperGenerators/CondGenerator.ts +++ b/packages/transpiler/view-generator/src/HelperGenerators/CondGenerator.ts @@ -1,113 +1,117 @@ -import { type types as t } from '@babel/core'; -import BaseGenerator from './BaseGenerator'; -import { Bitmap } from '@openinula/reactivity-parser'; - -export default class CondGenerator extends BaseGenerator { - /** - * @View - * $thisCond.cond = ${idx} - */ - geneCondIdx(idx: number): t.ExpressionStatement { - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')), - this.t.numericLiteral(idx) - ) - ); - } - - /** - * @View - * if ($thisCond.cond === ${idx}) { - * $thisCond.didntChange = true - * return [] - * } - */ - geneCondCheck(idx: number): t.IfStatement { - return this.t.ifStatement( - this.t.binaryExpression( - '===', - this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')), - this.t.numericLiteral(idx) - ), - this.t.blockStatement([ - this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('didntChange')), - this.t.booleanLiteral(true) - ) - ), - this.t.returnStatement(this.t.arrayExpression([])), - ]) - ); - } - - /** - * @View - * ${dlNodeName}?.updateCond(key) - */ - updateCondNodeCond(dlNodeName: string): t.Statement { - return this.optionalExpression( - dlNodeName, - this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('updateCond')), [ - ...this.updateParams.slice(1), - ]) - ); - } - - /** - * @View - * ${dlNodeName}?.update(changed) - */ - updateCondNode(dlNodeName: string): t.Statement { - return this.optionalExpression( - dlNodeName, - this.t.callExpression( - this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('update')), - this.updateParams - ) - ); - } - - /** - * @View - * ${dlNodeName} = new CondNode(${depNum}, ($thisCond) => {}) - */ - declareCondNode(dlNodeName: string, condFunc: t.BlockStatement, depMask: Bitmap): t.Statement { - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.identifier(dlNodeName), - this.t.newExpression(this.t.identifier(this.importMap.CondNode), [ - this.t.numericLiteral(depMask), - this.t.arrowFunctionExpression([this.t.identifier('$thisCond')], condFunc), - ]) - ) - ); - } - - /** - * return $thisCond.cond === ${branchIdx} ? [${nodeNames}] : $thisCond.updateCond() - */ - geneCondReturnStatement(nodeNames: string[], branchIdx: number): t.Statement { - // ---- If the returned cond is not the last one, - // it means it's been altered in the childrenNodes, - // so we update the cond again to get the right one - return this.t.returnStatement( - this.t.conditionalExpression( - this.t.binaryExpression( - '===', - this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')), - this.t.numericLiteral(branchIdx) - ), - this.t.arrayExpression(nodeNames.map(name => this.t.identifier(name))), - this.t.callExpression( - this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('updateCond')), - this.updateParams.slice(1) - ) - ) - ); - } -} +import { type types as t } from '@babel/core'; +import BaseGenerator from './BaseGenerator'; +import { Bitmap } from '@openinula/reactivity-parser'; +import { typeNode } from '../shard'; +import { InulaNodeType } from '@openinula/next-shared'; + +export default class CondGenerator extends BaseGenerator { + /** + * @View + * $thisCond.cond = ${idx} + */ + geneCondIdx(idx: number): t.ExpressionStatement { + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')), + this.t.numericLiteral(idx) + ) + ); + } + + /** + * @View + * if ($thisCond.cond === ${idx}) { + * $thisCond.didntChange = true + * return [] + * } + */ + geneCondCheck(idx: number): t.IfStatement { + return this.t.ifStatement( + this.t.binaryExpression( + '===', + this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')), + this.t.numericLiteral(idx) + ), + this.t.blockStatement([ + this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('didntChange')), + this.t.booleanLiteral(true) + ) + ), + this.t.returnStatement(this.t.arrayExpression([])), + ]) + ); + } + + /** + * @View + * updateNode(${nodeName}, key) + */ + updateCondNodeCond(nodeName: string): t.Statement { + return this.optionalExpression( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.updateNode), [ + this.t.identifier(nodeName), + ...this.updateParams.slice(1), + ]) + ); + } + + /** + * @View + * updateChildren(${nodeName}, changed) + */ + updateCondNode(nodeName: string): t.Statement { + return this.optionalExpression( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.updateChildren), [ + this.t.identifier(nodeName), + ...this.updateParams, + ]) + ); + } + + /** + * @View + * ${nodeName} = createNode(InulaNodeType.Cond, ${depNum}, ($thisCond) => {}) + */ + declareCondNode(nodeName: string, condFunc: t.BlockStatement, depMask: Bitmap): t.Statement { + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.identifier(nodeName), + this.t.callExpression(this.t.identifier(this.importMap.createNode), [ + typeNode(InulaNodeType.Cond), + this.t.numericLiteral(depMask), + this.t.arrowFunctionExpression([this.t.identifier('$thisCond')], condFunc), + ]) + ) + ); + } + + /** + * return $thisCond.cond === ${branchIdx} ? [${nodeNames}] : updateNode($thisCond) + */ + geneCondReturnStatement(nodeNames: string[], branchIdx: number): t.Statement { + // ---- If the returned cond is not the last one, + // it means it's been altered in the childrenNodes, + // so we update the cond again to get the right one + return this.t.returnStatement( + this.t.conditionalExpression( + this.t.binaryExpression( + '===', + this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('cond')), + this.t.numericLiteral(branchIdx) + ), + this.t.arrayExpression(nodeNames.map(name => this.t.identifier(name))), + this.t.callExpression(this.t.identifier(this.importMap.updateNode), [ + this.t.identifier('$thisCond'), + ...this.updateParams.slice(1), + ]) + ) + ); + } +} diff --git a/packages/transpiler/view-generator/src/HelperGenerators/ElementGenerator.ts b/packages/transpiler/view-generator/src/HelperGenerators/ElementGenerator.ts index c6ba5fd5abeae9bacea5179d9d6b11846630dd06..d07f33c8d714c6b750d89c60a89cb94fe2500b10 100644 --- a/packages/transpiler/view-generator/src/HelperGenerators/ElementGenerator.ts +++ b/packages/transpiler/view-generator/src/HelperGenerators/ElementGenerator.ts @@ -1,50 +1,50 @@ -import { type types as t } from '@babel/core'; -import PropViewGenerator from './PropViewGenerator'; - -export default class ElementGenerator extends PropViewGenerator { - /** - * @View - * el: - * View.addDidMount(${nodeName}, () => ( - * typeof ${value} === "function" ? ${value}($nodeEl) : ${value} = $nodeEl - * )) - * not el: - * typeof ${value} === "function" ? ${value}($nodeEl) : ${value} = $nodeEl - * @param nodeName - * @param value - * @param el true: nodeName._$el, false: nodeName - */ - initElement(nodeName: string, value: t.Expression, el = false): t.Statement { - const elNode = el - ? this.t.memberExpression(this.t.identifier(nodeName), this.t.identifier('_$el')) - : this.t.identifier(nodeName); - - const elementNode = this.t.conditionalExpression( - this.t.binaryExpression('===', this.t.unaryExpression('typeof', value, true), this.t.stringLiteral('function')), - this.t.callExpression(value, [elNode]), - this.t.assignmentExpression('=', value as t.LVal, elNode) - ); - - return el - ? this.t.expressionStatement( - this.t.callExpression(this.t.memberExpression(this.t.identifier('View'), this.t.identifier('addDidMount')), [ - this.t.identifier(nodeName), - this.t.arrowFunctionExpression([], elementNode), - ]) - ) - : this.t.expressionStatement(elementNode); - } - - // --- Utils - private isOnlyMemberExpression(value: t.Expression): boolean { - if (!this.t.isMemberExpression(value)) return false; - while (value.property) { - if (this.t.isMemberExpression(value.property)) { - value = value.property; - continue; - } else if (this.t.isIdentifier(value.property)) break; - else return false; - } - return true; - } -} +import { type types as t } from '@babel/core'; +import PropViewGenerator from './PropViewGenerator'; + +export default class ElementGenerator extends PropViewGenerator { + /** + * @View + * el: + * View.addDidMount(${nodeName}, () => ( + * typeof ${value} === "function" ? ${value}($nodeEl) : ${value} = $nodeEl + * )) + * not el: + * typeof ${value} === "function" ? ${value}($nodeEl) : ${value} = $nodeEl + * @param nodeName + * @param value + * @param el true: nodeName._$el, false: nodeName + */ + initElement(nodeName: string, value: t.Expression, el = false): t.Statement { + const elNode = el + ? this.t.memberExpression(this.t.identifier(nodeName), this.t.identifier('_$el')) + : this.t.identifier(nodeName); + + const elementNode = this.t.conditionalExpression( + this.t.binaryExpression('===', this.t.unaryExpression('typeof', value, true), this.t.stringLiteral('function')), + this.t.callExpression(value, [elNode]), + this.t.assignmentExpression('=', value as t.LVal, elNode) + ); + + return el + ? this.t.expressionStatement( + this.t.callExpression(this.t.memberExpression(this.t.identifier('View'), this.t.identifier('addDidMount')), [ + this.t.identifier(nodeName), + this.t.arrowFunctionExpression([], elementNode), + ]) + ) + : this.t.expressionStatement(elementNode); + } + + // --- Utils + private isOnlyMemberExpression(value: t.Expression): boolean { + if (!this.t.isMemberExpression(value)) return false; + while (value.property) { + if (this.t.isMemberExpression(value.property)) { + value = value.property; + continue; + } else if (this.t.isIdentifier(value.property)) break; + else return false; + } + return true; + } +} diff --git a/packages/transpiler/view-generator/src/HelperGenerators/ForwardPropGenerator.ts b/packages/transpiler/view-generator/src/HelperGenerators/ForwardPropGenerator.ts index 0dfcd599c592769b46ff2c00f7bc42e75d20e5f2..06407ffba3f6fc9cba24c0dc3074966db5cb3f92 100644 --- a/packages/transpiler/view-generator/src/HelperGenerators/ForwardPropGenerator.ts +++ b/packages/transpiler/view-generator/src/HelperGenerators/ForwardPropGenerator.ts @@ -1,16 +1,16 @@ -import { type types as t } from '@babel/core'; -import ElementGenerator from './ElementGenerator'; - -export default class ForwardPropsGenerator extends ElementGenerator { - /** - * @View - * this._$forwardProp(${dlNodeName}) - */ - forwardProps(dlNodeName: string): t.ExpressionStatement { - return this.t.expressionStatement( - this.t.callExpression(this.t.memberExpression(this.t.thisExpression(), this.t.identifier('_$addForwardProps')), [ - this.t.identifier(dlNodeName), - ]) - ); - } -} +import { type types as t } from '@babel/core'; +import ElementGenerator from './ElementGenerator'; + +export default class ForwardPropsGenerator extends ElementGenerator { + /** + * @View + * this._$forwardProp(${nodeName}) + */ + forwardProps(nodeName: string): t.ExpressionStatement { + return this.t.expressionStatement( + this.t.callExpression(this.t.memberExpression(this.t.thisExpression(), this.t.identifier('_$addForwardProps')), [ + this.t.identifier(nodeName), + ]) + ); + } +} diff --git a/packages/transpiler/view-generator/src/HelperGenerators/HTMLPropGenerator.ts b/packages/transpiler/view-generator/src/HelperGenerators/HTMLPropGenerator.ts index abc42bb6e8ac075f2f6d048dd6af32f073cfd159..af4e5d6ba41047ebae591eedc77d5b0536dd54ee 100644 --- a/packages/transpiler/view-generator/src/HelperGenerators/HTMLPropGenerator.ts +++ b/packages/transpiler/view-generator/src/HelperGenerators/HTMLPropGenerator.ts @@ -1,360 +1,355 @@ -import { type types as t } from '@babel/core'; -import { DLError } from '../error'; -import ForwardPropGenerator from './ForwardPropGenerator'; -import { Bitmap } from '@openinula/reactivity-parser'; - -export default class HTMLPropGenerator extends ForwardPropGenerator { - static DelegatedEvents = new Set([ - 'beforeinput', - 'click', - 'dblclick', - 'contextmenu', - 'focusin', - 'focusout', - 'input', - 'keydown', - 'keyup', - 'mousedown', - 'mousemove', - 'mouseout', - 'mouseover', - 'mouseup', - 'pointerdown', - 'pointermove', - 'pointerout', - 'pointerover', - 'pointerup', - 'touchend', - 'touchmove', - 'touchstart', - ]); - - /** - * @brief Add any HTML props according to the key - * @param name - * @param tag - * @param key - * @param value - * @param depMask - * @returns t.Statement - */ - addHTMLProp( - name: string, - tag: string, - key: string, - value: t.Expression, - depMask: Bitmap | undefined, - dependenciesNode: t.ArrayExpression - ): t.Statement | null { - // ---- Dynamic HTML prop with init and update - if (depMask && key !== 'ref') { - this.addUpdateStatements(depMask, this.setDynamicHTMLProp(name, tag, key, value, dependenciesNode, true)); - return this.setDynamicHTMLProp(name, tag, key, value, dependenciesNode, false); - } - // ---- Static HTML prop with init only - return this.setStaticHTMLProp(name, tag, key, value); - } - - /** - * @View - * insertNode(${dlNodeName}, ${childNodeName}, ${position}) - */ - insertNode(dlNodeName: string, childNodeName: string, position: number): t.ExpressionStatement { - return this.t.expressionStatement( - this.t.callExpression(this.t.identifier(this.importMap.insertNode), [ - this.t.identifier(dlNodeName), - this.t.identifier(childNodeName), - this.t.numericLiteral(position), - ]) - ); - } - - /** - * @View - * ${dlNodeName} && ${expression} - */ - private setPropWithCheck(dlNodeName: string, expression: t.Expression, check: boolean): t.Statement { - if (check) { - return this.optionalExpression(dlNodeName, expression); - } - return this.t.expressionStatement(expression); - } - - /** - * @View - * setStyle(${dlNodeName}, ${value}) - */ - private setHTMLStyle(dlNodeName: string, value: t.Expression, check: boolean): t.Statement { - return this.setPropWithCheck( - dlNodeName, - this.t.callExpression(this.t.identifier(this.importMap.setStyle), [this.t.identifier(dlNodeName), value]), - check - ); - } - - /** - * @View - * setStyle(${dlNodeName}, ${value}) - */ - private setHTMLDataset(dlNodeName: string, value: t.Expression, check: boolean): t.Statement { - return this.setPropWithCheck( - dlNodeName, - this.t.callExpression(this.t.identifier(this.importMap.setDataset), [this.t.identifier(dlNodeName), value]), - check - ); - } - - /** - * @View - * ${dlNodeName}.${key} = ${value} - */ - private setHTMLProp(dlNodeName: string, key: string, value: t.Expression): t.Statement { - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier(key)), - value - ) - ); - } - - /** - * @View - * ${dlNodeName}.setAttribute(${key}, ${value}) - */ - private setHTMLAttr(dlNodeName: string, key: string, value: t.Expression): t.Statement { - return this.t.expressionStatement( - this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('setAttribute')), [ - this.t.stringLiteral(key), - value, - ]) - ); - } - - /** - * @View - * ${dlNodeName}.addEventListener(${key}, ${value}) - */ - private setHTMLEvent(dlNodeName: string, key: string, value: t.Expression): t.Statement { - return this.t.expressionStatement( - this.t.callExpression( - this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('addEventListener')), - [this.t.stringLiteral(key), value] - ) - ); - } - - /** - * @View - * setEvent(${dlNodeName}, ${key}, ${value}) - */ - private setEvent(dlNodeName: string, key: string, value: t.Expression, check: boolean): t.Statement { - return this.setPropWithCheck( - dlNodeName, - this.t.callExpression(this.t.identifier(this.importMap.setEvent), [ - this.t.identifier(dlNodeName), - this.t.stringLiteral(key), - value, - ]), - check - ); - } - - /** - * @View - * delegateEvent(${dlNodeName}, ${key}, ${value}) - */ - private delegateEvent(dlNodeName: string, key: string, value: t.Expression, check: boolean): t.Statement { - this.config.wrapUpdate(value); - return this.setPropWithCheck( - dlNodeName, - this.t.callExpression(this.t.identifier(this.importMap.delegateEvent), [ - this.t.identifier(dlNodeName), - this.t.stringLiteral(key), - value, - ]), - check - ); - } - - /** - * @View - * setHTMLProp(${dlNodeName}, ${key}, ${valueFunc}, ${dependenciesNode}) - */ - private setCachedProp( - dlNodeName: string, - key: string, - value: t.Expression, - dependenciesNode: t.ArrayExpression, - check: boolean - ): t.Statement { - return this.setPropWithCheck( - dlNodeName, - this.t.callExpression(this.t.identifier(this.importMap.setHTMLProp), [ - this.t.identifier(dlNodeName), - this.t.stringLiteral(key), - this.t.arrowFunctionExpression([], value), - dependenciesNode, - ]), - check - ); - } - - /** - * @View - * setHTMLAttr(${dlNodeName}, ${key}, ${valueFunc}, ${dependenciesNode}, ${check}) - */ - private setCachedAttr( - dlNodeName: string, - key: string, - value: t.Expression, - dependenciesNode: t.ArrayExpression, - check: boolean - ): t.Statement { - return this.setPropWithCheck( - dlNodeName, - this.t.callExpression(this.t.identifier(this.importMap.setHTMLAttr), [ - this.t.identifier(dlNodeName), - this.t.stringLiteral(key), - this.t.arrowFunctionExpression([], value), - dependenciesNode, - ]), - check - ); - } - - /** - * @View - * setHTMLProps(${dlNodeName}, ${value}) - */ - private setHTMLPropObject(dlNodeName: string, value: t.Expression, check: boolean): t.Statement { - return this.setPropWithCheck( - dlNodeName, - this.t.callExpression(this.t.identifier(this.importMap.setHTMLProps), [this.t.identifier(dlNodeName), value]), - check - ); - } - - /** - * @View - * setHTMLAttrs(${dlNodeName}, ${value}) - */ - private setHTMLAttrObject(dlNodeName: string, value: t.Expression, check: boolean): t.Statement { - return this.setPropWithCheck( - dlNodeName, - this.t.callExpression(this.t.identifier(this.importMap.setHTMLAttrs), [this.t.identifier(dlNodeName), value]), - check - ); - } - - private static commonHTMLPropKeys = [ - 'style', - 'dataset', - 'props', - 'ref', - 'attrs', - 'forwardProps', - ...HTMLPropGenerator.lifecycle, - ]; - - /** - * For style/dataset/ref/attr/prop - */ - private addCommonHTMLProp( - dlNodeName: string, - attrName: string, - value: t.Expression, - check: boolean - ): t.Statement | null { - if (HTMLPropGenerator.lifecycle.includes(attrName as (typeof HTMLPropGenerator.lifecycle)[number])) { - if (!check) return this.addLifecycle(dlNodeName, attrName as (typeof HTMLPropGenerator.lifecycle)[number], value); - return null; - } - if (attrName === 'ref') { - if (!check) return this.initElement(dlNodeName, value); - return null; - } - if (attrName === 'style') return this.setHTMLStyle(dlNodeName, value, check); - if (attrName === 'dataset') return this.setHTMLDataset(dlNodeName, value, check); - if (attrName === 'props') return this.setHTMLPropObject(dlNodeName, value, check); - if (attrName === 'attrs') return this.setHTMLAttrObject(dlNodeName, value, check); - return DLError.throw2(); - } - - /** - * @View - * 1. Event listener - * - ${dlNodeName}.addEventListener(${key}, ${value}) - * 2. HTML internal attribute -> DOM property - * - ${dlNodeName}.${key} = ${value} - * 3. HTML custom attribute - * - ${dlNodeName}.setAttribute(${key}, ${value}) - */ - private setStaticHTMLProp( - dlNodeName: string, - tag: string, - attrName: string, - value: t.Expression - ): t.Statement | null { - if (HTMLPropGenerator.commonHTMLPropKeys.includes(attrName)) - return this.addCommonHTMLProp(dlNodeName, attrName, value, false); - if (attrName.startsWith('on')) { - const eventName = attrName.slice(2).toLowerCase(); - if (HTMLPropGenerator.DelegatedEvents.has(eventName)) { - return this.delegateEvent(dlNodeName, eventName, value, false); - } - return this.setHTMLEvent(dlNodeName, eventName, value); - } - if (this.isInternalAttribute(tag, attrName)) { - if (attrName === 'class') attrName = 'className'; - else if (attrName === 'for') attrName = 'htmlFor'; - return this.setHTMLProp(dlNodeName, attrName, value); - } - return this.setHTMLAttr(dlNodeName, attrName, value); - } - - /** - * @View - * 1. Event listener - * - ${setEvent}(${dlNodeName}, ${key}, ${value}) - * 2. HTML internal attribute -> DOM property - * - ${setHTMLProp}(${dlNodeName}, ${key}, ${value}) - * 3. HTML custom attribute - * - ${setHTMLAttr}(${dlNodeName}, ${key}, ${value}) - */ - private setDynamicHTMLProp( - dlNodeName: string, - tag: string, - attrName: string, - value: t.Expression, - dependenciesNode: t.ArrayExpression, - check: boolean - ): t.Statement | null { - if (HTMLPropGenerator.commonHTMLPropKeys.includes(attrName)) - return this.addCommonHTMLProp(dlNodeName, attrName, value, check); - if (attrName.startsWith('on')) { - const eventName = attrName.slice(2).toLowerCase(); - if (HTMLPropGenerator.DelegatedEvents.has(eventName)) { - return this.delegateEvent(dlNodeName, eventName, value, check); - } - return this.setEvent(dlNodeName, eventName, value, check); - } - if (this.alterAttributeMap[attrName]) { - attrName = this.alterAttributeMap[attrName]; - } - if (this.isInternalAttribute(tag, attrName)) { - return this.setCachedProp(dlNodeName, attrName, value, dependenciesNode, check); - } - return this.setCachedAttr(dlNodeName, attrName, value, dependenciesNode, check); - } - - /** - * @brief Check if the attribute is internal, i.e., can be accessed as js property - * @param tag - * @param attribute - * @returns true if the attribute is internal - */ - isInternalAttribute(tag: string, attribute: string): boolean { - return this.elementAttributeMap['*']?.includes(attribute) || this.elementAttributeMap[tag]?.includes(attribute); - } -} +import { type types as t } from '@babel/core'; +import { DLError } from '../error'; +import ForwardPropGenerator from './ForwardPropGenerator'; +import { Bitmap } from '@openinula/reactivity-parser'; + +export default class HTMLPropGenerator extends ForwardPropGenerator { + static DelegatedEvents = new Set([ + 'beforeinput', + 'click', + 'dblclick', + 'contextmenu', + 'focusin', + 'focusout', + 'input', + 'keydown', + 'keyup', + 'mousedown', + 'mousemove', + 'mouseout', + 'mouseover', + 'mouseup', + 'pointerdown', + 'pointermove', + 'pointerout', + 'pointerover', + 'pointerup', + 'touchend', + 'touchmove', + 'touchstart', + ]); + + /** + * @brief Add any HTML props according to the key + * @param name + * @param tag + * @param key + * @param value + * @param depMask + * @returns t.Statement + */ + addHTMLProp( + name: string, + tag: string, + key: string, + value: t.Expression, + depMask: Bitmap | undefined, + dependenciesNode: t.ArrayExpression + ): t.Statement | null { + // ---- Dynamic HTML prop with init and update + if (depMask && key !== 'ref') { + this.addUpdateStatements(depMask, this.setDynamicHTMLProp(name, tag, key, value, dependenciesNode, true)); + return this.setDynamicHTMLProp(name, tag, key, value, dependenciesNode, false); + } + // ---- Static HTML prop with init only + return this.setStaticHTMLProp(name, tag, key, value); + } + + /** + * @View + * insertNode(${nodeName}, ${childNodeName}, ${position}) + */ + insertNode(nodeName: string, childNodeName: string, position: number): t.ExpressionStatement { + return this.t.expressionStatement( + this.t.callExpression(this.t.identifier(this.importMap.insertNode), [ + this.t.identifier(nodeName), + this.t.identifier(childNodeName), + this.t.numericLiteral(position), + ]) + ); + } + + /** + * @View + * ${nodeName} && ${expression} + */ + private setPropWithCheck(nodeName: string, expression: t.Expression, check: boolean): t.Statement { + if (check) { + return this.optionalExpression(nodeName, expression); + } + return this.t.expressionStatement(expression); + } + + /** + * @View + * setStyle(${nodeName}, ${value}) + */ + private setHTMLStyle(nodeName: string, value: t.Expression, check: boolean): t.Statement { + return this.setPropWithCheck( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.setStyle), [this.t.identifier(nodeName), value]), + check + ); + } + + /** + * @View + * setStyle(${nodeName}, ${value}) + */ + private setHTMLDataset(nodeName: string, value: t.Expression, check: boolean): t.Statement { + return this.setPropWithCheck( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.setDataset), [this.t.identifier(nodeName), value]), + check + ); + } + + /** + * @View + * ${nodeName}.${key} = ${value} + */ + private setHTMLProp(nodeName: string, key: string, value: t.Expression): t.Statement { + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.memberExpression(this.t.identifier(nodeName), this.t.identifier(key)), + value + ) + ); + } + + /** + * @View + * ${nodeName}.setAttribute(${key}, ${value}) + */ + private setHTMLAttr(nodeName: string, key: string, value: t.Expression): t.Statement { + return this.t.expressionStatement( + this.t.callExpression(this.t.memberExpression(this.t.identifier(nodeName), this.t.identifier('setAttribute')), [ + this.t.stringLiteral(key), + value, + ]) + ); + } + + /** + * @View + * ${nodeName}.addEventListener(${key}, ${value}) + */ + private setHTMLEvent(nodeName: string, key: string, value: t.Expression): t.Statement { + return this.t.expressionStatement( + this.t.callExpression( + this.t.memberExpression(this.t.identifier(nodeName), this.t.identifier('addEventListener')), + [this.t.stringLiteral(key), value] + ) + ); + } + + /** + * @View + * setEvent(${nodeName}, ${key}, ${value}) + */ + private setEvent(nodeName: string, key: string, value: t.Expression, check: boolean): t.Statement { + return this.setPropWithCheck( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.setEvent), [ + this.t.identifier(nodeName), + this.t.stringLiteral(key), + value, + ]), + check + ); + } + + /** + * @View + * delegateEvent(${nodeName}, ${key}, ${value}) + */ + private delegateEvent(nodeName: string, key: string, value: t.Expression, check: boolean): t.Statement { + this.config.wrapUpdate(value); + return this.setPropWithCheck( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.delegateEvent), [ + this.t.identifier(nodeName), + this.t.stringLiteral(key), + value, + ]), + check + ); + } + + /** + * @View + * setHTMLProp(${nodeName}, ${key}, ${valueFunc}, ${dependenciesNode}) + */ + private setCachedProp( + nodeName: string, + key: string, + value: t.Expression, + dependenciesNode: t.ArrayExpression, + check: boolean + ): t.Statement { + return this.setPropWithCheck( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.setHTMLProp), [ + this.t.identifier(nodeName), + this.t.stringLiteral(key), + this.t.arrowFunctionExpression([], value), + dependenciesNode, + ]), + check + ); + } + + /** + * @View + * setHTMLAttr(${nodeName}, ${key}, ${valueFunc}, ${dependenciesNode}, ${check}) + */ + private setCachedAttr( + nodeName: string, + key: string, + value: t.Expression, + dependenciesNode: t.ArrayExpression, + check: boolean + ): t.Statement { + return this.setPropWithCheck( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.setHTMLAttr), [ + this.t.identifier(nodeName), + this.t.stringLiteral(key), + this.t.arrowFunctionExpression([], value), + dependenciesNode, + ]), + check + ); + } + + /** + * @View + * setHTMLProps(${nodeName}, ${value}) + */ + private setHTMLPropObject(nodeName: string, value: t.Expression, check: boolean): t.Statement { + return this.setPropWithCheck( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.setHTMLProps), [this.t.identifier(nodeName), value]), + check + ); + } + + /** + * @View + * setHTMLAttrs(${nodeName}, ${value}) + */ + private setHTMLAttrObject(nodeName: string, value: t.Expression, check: boolean): t.Statement { + return this.setPropWithCheck( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.setHTMLAttrs), [this.t.identifier(nodeName), value]), + check + ); + } + + private static commonHTMLPropKeys = [ + 'style', + 'dataset', + 'props', + 'ref', + 'attrs', + 'forwardProps', + ...HTMLPropGenerator.lifecycle, + ]; + + /** + * For style/dataset/ref/attr/prop + */ + private addCommonHTMLProp( + nodeName: string, + attrName: string, + value: t.Expression, + check: boolean + ): t.Statement | null { + if (HTMLPropGenerator.lifecycle.includes(attrName as (typeof HTMLPropGenerator.lifecycle)[number])) { + if (!check) return this.addLifecycle(nodeName, attrName as (typeof HTMLPropGenerator.lifecycle)[number], value); + return null; + } + if (attrName === 'ref') { + if (!check) return this.initElement(nodeName, value); + return null; + } + if (attrName === 'style') return this.setHTMLStyle(nodeName, value, check); + if (attrName === 'dataset') return this.setHTMLDataset(nodeName, value, check); + if (attrName === 'props') return this.setHTMLPropObject(nodeName, value, check); + if (attrName === 'attrs') return this.setHTMLAttrObject(nodeName, value, check); + return DLError.throw2(); + } + + /** + * @View + * 1. Event listener + * - ${nodeName}.addEventListener(${key}, ${value}) + * 2. HTML internal attribute -> DOM property + * - ${nodeName}.${key} = ${value} + * 3. HTML custom attribute + * - ${nodeName}.setAttribute(${key}, ${value}) + */ + private setStaticHTMLProp(nodeName: string, tag: string, attrName: string, value: t.Expression): t.Statement | null { + if (HTMLPropGenerator.commonHTMLPropKeys.includes(attrName)) + return this.addCommonHTMLProp(nodeName, attrName, value, false); + if (attrName.startsWith('on')) { + const eventName = attrName.slice(2).toLowerCase(); + if (HTMLPropGenerator.DelegatedEvents.has(eventName)) { + return this.delegateEvent(nodeName, eventName, value, false); + } + return this.setHTMLEvent(nodeName, eventName, value); + } + if (this.isInternalAttribute(tag, attrName)) { + if (attrName === 'class') attrName = 'className'; + else if (attrName === 'for') attrName = 'htmlFor'; + return this.setHTMLProp(nodeName, attrName, value); + } + return this.setHTMLAttr(nodeName, attrName, value); + } + + /** + * @View + * 1. Event listener + * - ${setEvent}(${nodeName}, ${key}, ${value}) + * 2. HTML internal attribute -> DOM property + * - ${setHTMLProp}(${nodeName}, ${key}, ${value}) + * 3. HTML custom attribute + * - ${setHTMLAttr}(${nodeName}, ${key}, ${value}) + */ + private setDynamicHTMLProp( + nodeName: string, + tag: string, + attrName: string, + value: t.Expression, + dependenciesNode: t.ArrayExpression, + check: boolean + ): t.Statement | null { + if (HTMLPropGenerator.commonHTMLPropKeys.includes(attrName)) + return this.addCommonHTMLProp(nodeName, attrName, value, check); + if (attrName.startsWith('on')) { + const eventName = attrName.slice(2).toLowerCase(); + if (HTMLPropGenerator.DelegatedEvents.has(eventName)) { + return this.delegateEvent(nodeName, eventName, value, check); + } + return this.setEvent(nodeName, eventName, value, check); + } + if (this.alterAttributeMap[attrName]) { + attrName = this.alterAttributeMap[attrName]; + } + if (this.isInternalAttribute(tag, attrName)) { + return this.setCachedProp(nodeName, attrName, value, dependenciesNode, check); + } + return this.setCachedAttr(nodeName, attrName, value, dependenciesNode, check); + } + + /** + * @brief Check if the attribute is internal, i.e., can be accessed as js property + * @param tag + * @param attribute + * @returns true if the attribute is internal + */ + isInternalAttribute(tag: string, attribute: string): boolean { + return this.elementAttributeMap['*']?.includes(attribute) || this.elementAttributeMap[tag]?.includes(attribute); + } +} diff --git a/packages/transpiler/view-generator/src/HelperGenerators/LifecycleGenerator.ts b/packages/transpiler/view-generator/src/HelperGenerators/LifecycleGenerator.ts index fe03727acdabc62ab795049d409a9a5e5e0f7dcd..395a175e6d346c7bf1b44cb44849585d1c9688d3 100644 --- a/packages/transpiler/view-generator/src/HelperGenerators/LifecycleGenerator.ts +++ b/packages/transpiler/view-generator/src/HelperGenerators/LifecycleGenerator.ts @@ -1,66 +1,62 @@ -import { type types as t } from '@babel/core'; -import BaseGenerator from './BaseGenerator'; - -export default class LifecycleGenerator extends BaseGenerator { - static lifecycle = ['willMount', 'didMount', 'willUnmount', 'didUnmount'] as const; - - /** - * @View - * ${dlNodeName} && ${value}(${dlNodeName}, changed) - */ - addOnUpdate(dlNodeName: string, value: t.Expression): t.Statement { - return this.t.expressionStatement( - this.t.logicalExpression( - '&&', - this.t.identifier(dlNodeName), - this.t.callExpression(value, [this.t.identifier(dlNodeName), ...this.updateParams.slice(1)]) - ) - ); - } - - /** - * @View - * willMount: - * - ${value}(${dlNodeName}) - * didMount/willUnmount/didUnmount: - * - View.addDidMount(${dlNodeName}, ${value}) - */ - addLifecycle( - dlNodeName: string, - key: (typeof LifecycleGenerator.lifecycle)[number], - value: t.Expression - ): t.Statement { - if (key === 'willMount') { - return this.addWillMount(dlNodeName, value); - } - return this.addOtherLifecycle(dlNodeName, value, key); - } - - /** - * @View - * ${value}(${dlNodeName}) - */ - addWillMount(dlNodeName: string, value: t.Expression): t.ExpressionStatement { - return this.t.expressionStatement(this.t.callExpression(value, [this.t.identifier(dlNodeName)])); - } - - /** - * @View - * View.addDidMount(${dlNodeName}, ${value}) - */ - addOtherLifecycle( - dlNodeName: string, - value: t.Expression, - type: 'didMount' | 'willUnmount' | 'didUnmount' - ): t.ExpressionStatement { - return this.t.expressionStatement( - this.t.callExpression( - this.t.memberExpression( - this.t.identifier('View'), - this.t.identifier(`add${type[0].toUpperCase()}${type.slice(1)}`) - ), - [this.t.identifier(dlNodeName), value] - ) - ); - } -} +import { type types as t } from '@babel/core'; +import BaseGenerator from './BaseGenerator'; + +export default class LifecycleGenerator extends BaseGenerator { + static lifecycle = ['willMount', 'didMount', 'willUnmount', 'didUnmount'] as const; + + /** + * @View + * ${nodeName} && ${value}(${nodeName}, changed) + */ + addOnUpdate(nodeName: string, value: t.Expression): t.Statement { + return this.t.expressionStatement( + this.t.logicalExpression( + '&&', + this.t.identifier(nodeName), + this.t.callExpression(value, [this.t.identifier(nodeName), ...this.updateParams.slice(1)]) + ) + ); + } + + /** + * @View + * willMount: + * - ${value}(${nodeName}) + * didMount/willUnmount/didUnmount: + * - View.addDidMount(${nodeName}, ${value}) + */ + addLifecycle(nodeName: string, key: (typeof LifecycleGenerator.lifecycle)[number], value: t.Expression): t.Statement { + if (key === 'willMount') { + return this.addWillMount(nodeName, value); + } + return this.addOtherLifecycle(nodeName, value, key); + } + + /** + * @View + * ${value}(${nodeName}) + */ + addWillMount(nodeName: string, value: t.Expression): t.ExpressionStatement { + return this.t.expressionStatement(this.t.callExpression(value, [this.t.identifier(nodeName)])); + } + + /** + * @View + * View.addDidMount(${nodeName}, ${value}) + */ + addOtherLifecycle( + nodeName: string, + value: t.Expression, + type: 'didMount' | 'willUnmount' | 'didUnmount' + ): t.ExpressionStatement { + return this.t.expressionStatement( + this.t.callExpression( + this.t.memberExpression( + this.t.identifier('View'), + this.t.identifier(`add${type[0].toUpperCase()}${type.slice(1)}`) + ), + [this.t.identifier(nodeName), value] + ) + ); + } +} diff --git a/packages/transpiler/view-generator/src/HelperGenerators/PropViewGenerator.ts b/packages/transpiler/view-generator/src/HelperGenerators/PropViewGenerator.ts index 17a46e6de573fcb9511054b2ab9b3d64431a5d7b..e659d5124848457ffe1aaba3358ed715b27b4f67 100644 --- a/packages/transpiler/view-generator/src/HelperGenerators/PropViewGenerator.ts +++ b/packages/transpiler/view-generator/src/HelperGenerators/PropViewGenerator.ts @@ -1,105 +1,104 @@ -import { type DependencyProp, type ViewParticle } from '@openinula/reactivity-parser'; -import LifecycleGenerator from './LifecycleGenerator'; - -export default class PropViewGenerator extends LifecycleGenerator { - /** - * @brief Alter prop view in batch - * @param props - * @returns altered props - */ - alterPropViews | undefined>(props: T): T { - if (!props) return props; - return Object.fromEntries( - Object.entries(props).map(([key, prop]) => { - return [key, this.alterPropView(prop)!]; - }) - ) as T; - } - - /** - * @View - * ${dlNodeName} = new PropView(($addUpdate) => { - * addUpdate((changed) => { ${updateStatements} }) - * ${initStatements} - * return ${topLevelNodes} - * }) - */ - declarePropView(viewParticles: ViewParticle[]) { - // ---- Generate PropView - const [initStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren( - viewParticles, - false, - true - ); - // ---- Add update function to the first node - /** - * $addUpdate((changed) => { ${updateStatements} }) - */ - if (Object.keys(updateStatements).length > 0) { - initStatements.unshift( - this.t.expressionStatement( - this.t.callExpression(this.t.identifier('$addUpdate'), [ - this.t.arrowFunctionExpression(this.updateParams, this.geneUpdateBody(updateStatements)), - ]) - ) - ); - } - initStatements.unshift(...this.declareNodes(nodeIdx)); - initStatements.push(this.generateReturnStatement(topLevelNodes)); - - // ---- Assign as a dlNode - const dlNodeName = this.generateNodeName(); - const propViewNode = this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.identifier(dlNodeName), - this.t.newExpression(this.t.identifier(this.importMap.PropView), [ - this.t.arrowFunctionExpression([this.t.identifier('$addUpdate')], this.t.blockStatement(initStatements)), - ]) - ) - ); - this.addInitStatement(propViewNode); - const propViewIdentifier = this.t.identifier(dlNodeName); - - // ---- Add to update statements - /** - * ${dlNodeName}?.update(changed) - */ - this.addUpdateStatementsWithoutDep( - this.optionalExpression( - dlNodeName, - this.t.callExpression( - this.t.memberExpression(propViewIdentifier, this.t.identifier('update')), - this.updateParams - ) - ) - ); - - return dlNodeName; - } - - /** - * @brief Alter prop view by replacing prop view with a recursively generated prop view - * @param prop - * @returns altered prop - */ - alterPropView(prop: T): T { - if (!prop) return prop; - const { value, viewPropMap } = prop; - if (!viewPropMap) return { ...prop, value }; - let newValue = value; - this.traverse(this.valueWrapper(value), { - StringLiteral: innerPath => { - const id = innerPath.node.value; - const viewParticles = viewPropMap[id]; - if (!viewParticles) return; - const propViewIdentifier = this.t.identifier(this.declarePropView(viewParticles)); - - if (value === innerPath.node) newValue = propViewIdentifier; - innerPath.replaceWith(propViewIdentifier); - innerPath.skip(); - }, - }); - return { ...prop, value: newValue }; - } -} +import { type DependencyProp, type ViewParticle } from '@openinula/reactivity-parser'; +import LifecycleGenerator from './LifecycleGenerator'; +import { InulaNodeType } from '@openinula/next-shared'; +import { typeNode } from '../shard'; + +export default class PropViewGenerator extends LifecycleGenerator { + /** + * @brief Alter prop view in batch + * @param props + * @returns altered props + */ + alterPropViews | undefined>(props: T): T { + if (!props) return props; + return Object.fromEntries( + Object.entries(props).map(([key, prop]) => { + return [key, this.alterPropView(prop)!]; + }) + ) as T; + } + + /** + * @View + * ${nodeName} = createNode(InulaNodeType.Children, ($addUpdate) => { + * addUpdate((changed) => { ${updateStatements} }) + * ${initStatements} + * return ${topLevelNodes} + * }) + */ + declarePropView(viewParticles: ViewParticle[]) { + // ---- Generate PropView + const [initStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren( + viewParticles, + false, + true + ); + // ---- Add update function to the first node + /** + * $addUpdate((changed) => { ${updateStatements} }) + */ + if (Object.keys(updateStatements).length > 0) { + initStatements.unshift( + this.t.expressionStatement( + this.t.callExpression(this.t.identifier('$addUpdate'), [ + this.t.arrowFunctionExpression(this.updateParams, this.geneUpdateBody(updateStatements)), + ]) + ) + ); + } + initStatements.unshift(...this.declareNodes(nodeIdx)); + initStatements.push(this.generateReturnStatement(topLevelNodes)); + + // ---- Assign as a children node + const nodeName = this.generateNodeName(); + const propViewNode = this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.identifier(nodeName), + this.t.callExpression(this.t.identifier(this.importMap.createNode), [ + typeNode(InulaNodeType.Children), + this.t.arrowFunctionExpression([this.t.identifier('$addUpdate')], this.t.blockStatement(initStatements)), + ]) + ) + ); + this.addInitStatement(propViewNode); + const propViewIdentifier = this.t.identifier(nodeName); + + // ---- Add to update statements + /** + * updateNode(${nodeName}, changed) + */ + this.addUpdateStatementsWithoutDep( + this.t.expressionStatement( + this.t.callExpression(this.t.identifier(this.importMap.updateNode), [propViewIdentifier, ...this.updateParams]) + ) + ); + + return nodeName; + } + + /** + * @brief Alter prop view by replacing prop view with a recursively generated prop view + * @param prop + * @returns altered prop + */ + alterPropView(prop: T): T { + if (!prop) return prop; + const { value, viewPropMap } = prop; + if (!viewPropMap) return { ...prop, value }; + let newValue = value; + this.traverse(this.valueWrapper(value), { + StringLiteral: innerPath => { + const id = innerPath.node.value; + const viewParticles = viewPropMap[id]; + if (!viewParticles) return; + const propViewIdentifier = this.t.identifier(this.declarePropView(viewParticles)); + + if (value === innerPath.node) newValue = propViewIdentifier; + innerPath.replaceWith(propViewIdentifier); + innerPath.skip(); + }, + }); + return { ...prop, value: newValue }; + } +} diff --git a/packages/transpiler/view-generator/src/MainViewGenerator.ts b/packages/transpiler/view-generator/src/MainViewGenerator.ts index a6396c101fcbc4d8a83bd00b3cb84870b1d399ac..7ba2932d7191a39a41efdd3ca2e11b2dfc3e5f3b 100644 --- a/packages/transpiler/view-generator/src/MainViewGenerator.ts +++ b/packages/transpiler/view-generator/src/MainViewGenerator.ts @@ -1,76 +1,76 @@ -import { type types as t } from '@babel/core'; -import { type ViewParticle } from '@openinula/reactivity-parser'; -import ViewGenerator from './ViewGenerator'; - -export default class MainViewGenerator extends ViewGenerator { - /** - * @brief Generate the main view, i.e., View() { ... } - * @param viewParticles - * @returns [viewBody, classProperties, templateIdx] - */ - generate( - viewParticles: ViewParticle[] - ): [t.ArrowFunctionExpression | null, t.Statement[], t.Statement[], t.ArrayExpression] { - const allTemplates: t.VariableDeclaration[] = []; - const allInitStatements: t.Statement[] = []; - const allUpdateStatements: Record = {}; - const topLevelNodes: string[] = []; - - viewParticles.forEach(viewParticle => { - const [initStatements, updateStatements, templates, nodeName] = this.generateChild(viewParticle); - allInitStatements.push(...initStatements); - Object.entries(updateStatements).forEach(([depNum, statements]) => { - if (!allUpdateStatements[Number(depNum)]) { - allUpdateStatements[Number(depNum)] = []; - } - allUpdateStatements[Number(depNum)].push(...statements); - }); - allTemplates.push(...templates); - topLevelNodes.push(nodeName); - }); - - // Sequence of statements in the view: - // 1. Declare all nodes(the variables that hold the dom nodes) - // 2. Declare all variables(temporary variables) - // 3. Declare all init statements(to create dom nodes) - const nodeInitStmt = [...this.declareNodes(), ...allInitStatements]; - const topLevelNodesArray = this.t.arrayExpression(topLevelNodes.map(nodeName => this.t.identifier(nodeName))); - return [this.geneUpdate(allUpdateStatements, topLevelNodes), nodeInitStmt, allTemplates, topLevelNodesArray]; - } - - /** - * @View - * this._$update = ($changed) => { - * if ($changed & 1) { - * ... - * } - * ... - * } - */ - private geneUpdate(updateStatements: Record, topLevelNodes: string[]) { - if (Object.keys(updateStatements).length === 0) return null; - return this.t.arrowFunctionExpression( - this.updateParams, - this.t.blockStatement([ - ...Object.entries(updateStatements) - .filter(([depNum]) => depNum !== '0') - .map(([depNum, statements]) => { - return this.t.ifStatement( - this.t.binaryExpression('&', this.t.identifier('$changed'), this.t.numericLiteral(Number(depNum))), - this.t.blockStatement(statements) - ); - }), - ...(updateStatements[0] ?? []), - this.geneReturn(topLevelNodes), - ]) - ); - } - - /** - * @View - * return [${nodeNames}] - */ - private geneReturn(topLevelNodes: string[]) { - return this.t.returnStatement(this.t.arrayExpression(topLevelNodes.map(nodeName => this.t.identifier(nodeName)))); - } -} +import { type types as t } from '@babel/core'; +import { type ViewParticle } from '@openinula/reactivity-parser'; +import ViewGenerator from './ViewGenerator'; + +export default class MainViewGenerator extends ViewGenerator { + /** + * @brief Generate the main view, i.e., View() { ... } + * @param viewParticles + * @returns [viewBody, classProperties, templateIdx] + */ + generate( + viewParticles: ViewParticle[] + ): [t.ArrowFunctionExpression | null, t.Statement[], t.Statement[], t.ArrayExpression] { + const allTemplates: t.VariableDeclaration[] = []; + const allInitStatements: t.Statement[] = []; + const allUpdateStatements: Record = {}; + const topLevelNodes: string[] = []; + + viewParticles.forEach(viewParticle => { + const [initStatements, updateStatements, templates, nodeName] = this.generateChild(viewParticle); + allInitStatements.push(...initStatements); + Object.entries(updateStatements).forEach(([depNum, statements]) => { + if (!allUpdateStatements[Number(depNum)]) { + allUpdateStatements[Number(depNum)] = []; + } + allUpdateStatements[Number(depNum)].push(...statements); + }); + allTemplates.push(...templates); + topLevelNodes.push(nodeName); + }); + + // Sequence of statements in the view: + // 1. Declare all nodes(the variables that hold the dom nodes) + // 2. Declare all variables(temporary variables) + // 3. Declare all init statements(to create dom nodes) + const nodeInitStmt = [...this.declareNodes(), ...allInitStatements]; + const topLevelNodesArray = this.t.arrayExpression(topLevelNodes.map(nodeName => this.t.identifier(nodeName))); + return [this.geneUpdate(allUpdateStatements, topLevelNodes), nodeInitStmt, allTemplates, topLevelNodesArray]; + } + + /** + * @View + * this._$update = ($changed) => { + * if ($changed & 1) { + * ... + * } + * ... + * } + */ + private geneUpdate(updateStatements: Record, topLevelNodes: string[]) { + if (Object.keys(updateStatements).length === 0) return null; + return this.t.arrowFunctionExpression( + this.updateParams, + this.t.blockStatement([ + ...Object.entries(updateStatements) + .filter(([depNum]) => depNum !== '0') + .map(([depNum, statements]) => { + return this.t.ifStatement( + this.t.binaryExpression('&', this.t.identifier('$changed'), this.t.numericLiteral(Number(depNum))), + this.t.blockStatement(statements) + ); + }), + ...(updateStatements[0] ?? []), + this.geneReturn(topLevelNodes), + ]) + ); + } + + /** + * @View + * return [${nodeNames}] + */ + private geneReturn(topLevelNodes: string[]) { + return this.t.returnStatement(this.t.arrayExpression(topLevelNodes.map(nodeName => this.t.identifier(nodeName)))); + } +} diff --git a/packages/transpiler/view-generator/src/NodeGenerators/CompGenerator.ts b/packages/transpiler/view-generator/src/NodeGenerators/CompGenerator.ts index d99cb793e34c491449285d33f109b18f0d3a4677..deb05f79f4ff92d8d17d47381683cbbf1c421e40 100644 --- a/packages/transpiler/view-generator/src/NodeGenerators/CompGenerator.ts +++ b/packages/transpiler/view-generator/src/NodeGenerators/CompGenerator.ts @@ -1,174 +1,171 @@ -import { type types as t } from '@babel/core'; -import { type DependencyProp, type CompParticle, type ViewParticle, Bitmap } from '@openinula/reactivity-parser'; -import ForwardPropGenerator from '../HelperGenerators/ForwardPropGenerator'; - -export default class CompGenerator extends ForwardPropGenerator { - run() { - let { props } = this.viewParticle as CompParticle; - props = this.alterPropViews(props); - const { tag, children } = this.viewParticle as CompParticle; - - const nodeName = this.generateNodeName(); - - this.addInitStatement(...this.declareCompNode(nodeName, tag, props, children)); - - // ---- Resolve props - Object.entries(props).forEach(([key, { value, depMask, dependenciesNode }]) => { - if (key === 'forwardProps') return; - if (key === 'didUpdate') return; - if (key === 'ref') return; - - if (CompGenerator.lifecycle.includes(key as (typeof CompGenerator.lifecycle)[number])) { - this.addInitStatement(this.addLifecycle(nodeName, key as (typeof CompGenerator.lifecycle)[number], value)); - return; - } - - if (key === 'props') { - this.addUpdateStatements(depMask, this.setCompProps(nodeName, value, dependenciesNode)); - return; - } - - this.addUpdateStatements(depMask, this.setCompProp(nodeName, key, value, dependenciesNode)); - }); - - // ---- sub component update - if (this.t.isIdentifier(tag)) { - const depMask = this.config.subComps.find(([name]) => name === tag.name)?.[1]; - if (depMask) { - this.addUpdateStatements(depMask, this.geneSubCompOnUpdate(nodeName, depMask)); - } - } - return nodeName; - } - - /** - * @View - * return Props object - */ - private generateCompProps(props: Record): t.Expression { - if (Object.keys(props).length === 0) return this.t.objectExpression([]); - - return this.t.objectExpression( - Object.entries(props).map(([key, value]) => { - this.config.wrapUpdate(value); - return this.t.objectProperty(this.t.stringLiteral(key), value); - }) - ); - } - - /** - * @View - * ${dlNodeName} = ${tag}() - */ - private declareCompNode( - dlNodeName: string, - tag: t.Expression, - props: Record, - children: ViewParticle[] - ): t.Statement[] { - const newProps = Object.fromEntries( - Object.entries(props) - .filter(([key]) => !['elements', '_$content', 'didUpdate', 'props', ...CompGenerator.lifecycle].includes(key)) - .map(([key, { value }]) => [key, key === 'ref' ? this.wrapRefHandler(value) : value]) - ); - - if (children.length > 0) { - newProps.children = this.t.identifier(this.declarePropView(children)); - } - - return [ - this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.identifier(dlNodeName), - this.t.callExpression(this.t.identifier(this.config.importMap.Comp), [tag, this.generateCompProps(newProps)]) - ) - ), - ]; - } - - private wrapRefHandler(refVal: t.Expression) { - const refInput = this.t.identifier('$el'); - return this.t.functionExpression( - null, - [refInput], - this.t.blockStatement([ - this.t.expressionStatement( - this.t.conditionalExpression( - this.t.binaryExpression( - '===', - this.t.unaryExpression('typeof', refVal, true), - this.t.stringLiteral('function') - ), - this.t.callExpression(refVal, [refInput]), - this.t.assignmentExpression('=', refVal as t.LVal, refInput) - ) - ), - ]) - ); - } - - /** - * @View - * ${dlNodeName}._$setContent(() => ${value}, ${dependenciesNode}) - */ - private setCompContent(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement { - return this.optionalExpression( - dlNodeName, - this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$setContent')), [ - this.t.arrowFunctionExpression([], value), - dependenciesNode, - ]) - ); - } - - /** - * @View - * ${dlNodeName}._$setProp(${key}, () => ${value}, ${dependenciesNode}) - */ - private setCompProp( - dlNodeName: string, - key: string, - value: t.Expression, - dependenciesNode: t.ArrayExpression - ): t.Statement { - return this.optionalExpression( - dlNodeName, - this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$setProp')), [ - this.t.stringLiteral(key), - this.t.arrowFunctionExpression([], value), - dependenciesNode, - ]) - ); - } - - /** - * @View - * ${dlNodeName}._$setProps(() => ${value}, ${dependenciesNode}) - */ - private setCompProps(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement { - return this.optionalExpression( - dlNodeName, - this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$setProps')), [ - this.t.arrowFunctionExpression([], value), - dependenciesNode, - ]) - ); - } - - /** - * @View - * ${nodeName}.updateState(${depMask}) - * @param nodeName - * @param depMask - * @private - */ - private geneSubCompOnUpdate(nodeName: string, depMask: number) { - return this.t.expressionStatement( - this.t.callExpression(this.t.memberExpression(this.t.identifier(nodeName), this.t.identifier('updateDerived')), [ - this.t.nullLiteral(), // the first parameter should be given. - this.t.numericLiteral(depMask), - ]) - ); - } -} +import { type types as t } from '@babel/core'; +import { type DependencyProp, type CompParticle, type ViewParticle, Bitmap } from '@openinula/reactivity-parser'; +import ForwardPropGenerator from '../HelperGenerators/ForwardPropGenerator'; + +export default class CompGenerator extends ForwardPropGenerator { + run() { + let { props } = this.viewParticle as CompParticle; + props = this.alterPropViews(props); + const { tag, children } = this.viewParticle as CompParticle; + + const nodeName = this.generateNodeName(); + + this.addInitStatement(...this.declareCompNode(nodeName, tag, props, children)); + + // ---- Resolve props + Object.entries(props).forEach(([key, { value, depMask, dependenciesNode }]) => { + if (key === 'forwardProps') return; + if (key === 'didUpdate') return; + if (key === 'ref') return; + + if (CompGenerator.lifecycle.includes(key as (typeof CompGenerator.lifecycle)[number])) { + this.addInitStatement(this.addLifecycle(nodeName, key as (typeof CompGenerator.lifecycle)[number], value)); + return; + } + + this.addUpdateStatements(depMask, this.setCompProp(nodeName, key, value, dependenciesNode)); + }); + + // ---- sub component update + if (this.t.isIdentifier(tag)) { + const depMask = this.config.subComps.find(([name]) => name === tag.name)?.[1]; + if (depMask) { + this.addUpdateStatements(depMask, this.geneSubCompOnUpdate(nodeName, depMask)); + } + } + return nodeName; + } + + /** + * @View + * return Props object + */ + private generateCompProps(props: Record): t.Expression { + if (Object.keys(props).length === 0) return this.t.objectExpression([]); + + return this.t.objectExpression( + Object.entries(props).map(([key, value]) => { + this.config.wrapUpdate(value); + return this.t.objectProperty(this.t.stringLiteral(key), value); + }) + ); + } + + /** + * @View + * ${nodeName} = ${tag}() + */ + private declareCompNode( + nodeName: string, + tag: t.Expression, + props: Record, + children: ViewParticle[] + ): t.Statement[] { + const newProps = Object.fromEntries( + Object.entries(props) + .filter(([key]) => !['elements', '_$content', 'didUpdate', 'props', ...CompGenerator.lifecycle].includes(key)) + .map(([key, { value }]) => [key, key === 'ref' ? this.wrapRefHandler(value) : value]) + ); + + if (children.length > 0) { + newProps.children = this.t.identifier(this.declarePropView(children)); + } + + return [ + this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.identifier(nodeName), + this.t.callExpression(this.t.identifier(this.config.importMap.Comp), [tag, this.generateCompProps(newProps)]) + ) + ), + ]; + } + + private wrapRefHandler(refVal: t.Expression) { + const refInput = this.t.identifier('$el'); + return this.t.functionExpression( + null, + [refInput], + this.t.blockStatement([ + this.t.expressionStatement( + this.t.conditionalExpression( + this.t.binaryExpression( + '===', + this.t.unaryExpression('typeof', refVal, true), + this.t.stringLiteral('function') + ), + this.t.callExpression(refVal, [refInput]), + this.t.assignmentExpression('=', refVal as t.LVal, refInput) + ) + ), + ]) + ); + } + + /** + * @View + * ${nodeName}._$setContent(() => ${value}, ${dependenciesNode}) + */ + // private setCompContent(nodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement { + // return this.optionalExpression( + // nodeName, + // this.t.callExpression(this.t.memberExpression(this.t.identifier(nodeName), this.t.identifier('_$setContent')), [ + // this.t.arrowFunctionExpression([], value), + // dependenciesNode, + // ]) + // ); + // } + + /** + * @View + * setProp(${nodeName}, ${key}, () => ${value}, ${dependenciesNode}) + */ + private setCompProp( + nodeName: string, + key: string, + value: t.Expression, + dependenciesNode: t.ArrayExpression + ): t.Statement { + return this.optionalExpression( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.setProp), [ + this.t.identifier(nodeName), + this.t.stringLiteral(key), + this.t.arrowFunctionExpression([], value), + dependenciesNode, + ]) + ); + } + + /** + * @View + * ${nodeName}._$setProps(() => ${value}, ${dependenciesNode}) + */ + // private setCompProps(nodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement { + // return this.optionalExpression( + // nodeName, + // this.t.callExpression(this.t.memberExpression(this.t.identifier(nodeName), this.t.identifier('_$setProps')), [ + // this.t.arrowFunctionExpression([], value), + // dependenciesNode, + // ]) + // ); + // } + + /** + * @View + * updateNode(${nodeName}, ${depMask}) + * @param nodeName + * @param depMask + * @private + */ + private geneSubCompOnUpdate(nodeName: string, depMask: number) { + return this.t.expressionStatement( + this.t.callExpression(this.t.identifier(this.importMap.updateNode), [ + this.t.identifier(nodeName), + this.t.nullLiteral(), // the first parameter should be given. + this.t.numericLiteral(depMask), + ]) + ); + } +} diff --git a/packages/transpiler/view-generator/src/NodeGenerators/ContextGenerator.ts b/packages/transpiler/view-generator/src/NodeGenerators/ContextGenerator.ts index 40344f31c5b7716a3ca6e699bf5c6962c56f5ee8..8380b88fbb28330d05557c48bf5fe325a1fa8eeb 100644 --- a/packages/transpiler/view-generator/src/NodeGenerators/ContextGenerator.ts +++ b/packages/transpiler/view-generator/src/NodeGenerators/ContextGenerator.ts @@ -1,98 +1,104 @@ -import { type types as t } from '@babel/core'; -import { type ViewParticle, type DependencyProp, type ContextParticle } from '@openinula/reactivity-parser'; -import PropViewGenerator from '../HelperGenerators/PropViewGenerator'; - -export default class ContextGenerator extends PropViewGenerator { - run() { - // eslint-disable-next-line prefer-const - let { props, contextName } = this.viewParticle as ContextParticle; - props = this.alterPropViews(props)!; - const { children } = this.viewParticle as ContextParticle; - - const dlNodeName = this.generateNodeName(); - - this.addInitStatement(this.declareEnvNode(dlNodeName, props, contextName)); - - // ---- Children - this.addInitStatement(this.geneEnvChildren(dlNodeName, children)); - - // ---- Update props - Object.entries(props).forEach(([key, { depMask, value, dependenciesNode }]) => { - if (!depMask) return; - this.addUpdateStatements(depMask, this.updateContext(dlNodeName, key, value, dependenciesNode)); - }); - - return dlNodeName; - } - - /** - * @View - * { ${key}: ${value}, ... } - * { ${key}: ${deps}, ... } - */ - private generateEnvs(props: Record): t.Expression[] { - return [ - this.t.objectExpression( - Object.entries(props).map(([key, { value }]) => this.t.objectProperty(this.t.identifier(key), value)) - ), - this.t.objectExpression( - Object.entries(props) - .map( - ([key, { dependenciesNode }]) => - dependenciesNode && this.t.objectProperty(this.t.identifier(key), dependenciesNode) - ) - .filter(Boolean) as t.ObjectProperty[] - ), - ]; - } - - /** - * @View - * ${dlNodeName} = new ContextProvider(context, envs) - */ - private declareEnvNode(dlNodeName: string, props: Record, contextName: string): t.Statement { - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.identifier(dlNodeName), - this.t.newExpression(this.t.identifier(this.importMap.ContextProvider), [ - this.t.identifier(contextName), - ...this.generateEnvs(props), - ]) - ) - ); - } - - /** - * @View - * ${dlNodeName}.initNodes([${childrenNames}]) - */ - private geneEnvChildren(dlNodeName: string, children: ViewParticle[]): t.Statement { - const [statements, childrenNames] = this.generateChildren(children); - this.addInitStatement(...statements); - return this.t.expressionStatement( - this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('initNodes')), [ - this.t.arrayExpression(childrenNames.map(name => this.t.identifier(name))), - ]) - ); - } - - /** - * @View - * ${dlNodeName}.updateContext(${key}, () => ${value}, ${dependenciesNode}) - */ - private updateContext( - dlNodeName: string, - key: string, - value: t.Expression, - dependenciesNode: t.ArrayExpression - ): t.Statement { - return this.optionalExpression( - dlNodeName, - this.t.callExpression( - this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('updateContext')), - [this.t.stringLiteral(key), this.t.arrowFunctionExpression([], value), dependenciesNode] - ) - ); - } -} +import { type types as t } from '@babel/core'; +import { type ViewParticle, type DependencyProp, type ContextParticle } from '@openinula/reactivity-parser'; +import PropViewGenerator from '../HelperGenerators/PropViewGenerator'; +import { InulaNodeType } from '@openinula/next-shared'; +import { typeNode } from '../shard'; + +export default class ContextGenerator extends PropViewGenerator { + run() { + // eslint-disable-next-line prefer-const + let { props, contextName } = this.viewParticle as ContextParticle; + props = this.alterPropViews(props)!; + const { children } = this.viewParticle as ContextParticle; + + const nodeName = this.generateNodeName(); + + this.addInitStatement(this.declareEnvNode(nodeName, props, contextName)); + + // ---- Children + this.addInitStatement(this.geneEnvChildren(nodeName, children)); + + // ---- Update props + Object.entries(props).forEach(([key, { depMask, value, dependenciesNode }]) => { + if (!depMask) return; + this.addUpdateStatements(depMask, this.updateContext(nodeName, key, value, dependenciesNode)); + }); + + return nodeName; + } + + /** + * @View + * { ${key}: ${value}, ... } // contextValues + * { ${key}: ${deps}, ... } // contextDeps + */ + private generateContext(props: Record): t.Expression[] { + return [ + this.t.objectExpression( + Object.entries(props).map(([key, { value }]) => this.t.objectProperty(this.t.identifier(key), value)) + ), + this.t.objectExpression( + Object.entries(props) + .map( + ([key, { dependenciesNode }]) => + dependenciesNode && this.t.objectProperty(this.t.identifier(key), dependenciesNode) + ) + .filter(Boolean) as t.ObjectProperty[] + ), + ]; + } + + /** + * @View + * ${nodeName} = createNode(InulaNodeType.Context, context, contextValues, contextDeps) + */ + private declareEnvNode(nodeName: string, props: Record, contextName: string): t.Statement { + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.identifier(nodeName), + this.t.callExpression(this.t.identifier(this.importMap.createNode), [ + typeNode(InulaNodeType.Context), + this.t.identifier(contextName), + ...this.generateContext(props), + ]) + ) + ); + } + + /** + * @View + * initContextChildren(${nodeName}, [${childrenNames}]) + */ + private geneEnvChildren(nodeName: string, children: ViewParticle[]): t.Statement { + const [statements, childrenNames] = this.generateChildren(children); + this.addInitStatement(...statements); + return this.t.expressionStatement( + this.t.callExpression(this.t.identifier(this.importMap.initContextChildren), [ + this.t.identifier(nodeName), + this.t.arrayExpression(childrenNames.map(name => this.t.identifier(name))), + ]) + ); + } + + /** + * @View + * updateNode(${nodeName}, ${key}, () => ${value}, ${dependenciesNode}) + */ + private updateContext( + nodeName: string, + key: string, + value: t.Expression, + dependenciesNode: t.ArrayExpression + ): t.Statement { + return this.optionalExpression( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.updateNode), [ + this.t.identifier(nodeName), + this.t.stringLiteral(key), + this.t.arrowFunctionExpression([], value), + dependenciesNode, + ]) + ); + } +} diff --git a/packages/transpiler/view-generator/src/NodeGenerators/ExpGenerator.ts b/packages/transpiler/view-generator/src/NodeGenerators/ExpGenerator.ts index ead21d18a927d42a09ca191fda71cc9d18086d87..a55480a42df0b7f7f6bec89eab852691bcdac376 100644 --- a/packages/transpiler/view-generator/src/NodeGenerators/ExpGenerator.ts +++ b/packages/transpiler/view-generator/src/NodeGenerators/ExpGenerator.ts @@ -1,54 +1,55 @@ -import { type types as t } from '@babel/core'; -import { type ExpParticle } from '@openinula/reactivity-parser'; -import ElementGenerator from '../HelperGenerators/ElementGenerator'; - -export default class ExpGenerator extends ElementGenerator { - run() { - let { content } = this.viewParticle as ExpParticle; - content = this.alterPropView(content)!; - - const dlNodeName = this.generateNodeName(); - - this.addInitStatement(this.declareExpNode(dlNodeName, content.value, content.dependenciesNode)); - - if (content.depMask) { - this.addUpdateStatements( - content.depMask, - this.updateExpNode(dlNodeName, content.value, content.dependenciesNode) - ); - } - - return dlNodeName; - } - - /** - * @View - * ${dlNodeName} = new ExpNode(${value}, dependenciesNode) - */ - private declareExpNode(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement { - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.identifier(dlNodeName), - this.t.newExpression(this.t.identifier(this.importMap.ExpNode), [ - value, - dependenciesNode ?? this.t.nullLiteral(), - ]) - ) - ); - } - - /** - * @View - * ${dlNodeName}.update(() => value, dependenciesNode) - */ - private updateExpNode(dlNodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement { - return this.optionalExpression( - dlNodeName, - this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('update')), [ - this.t.arrowFunctionExpression([], value), - dependenciesNode ?? this.t.nullLiteral(), - ]) - ); - } -} +import { type types as t } from '@babel/core'; +import { type ExpParticle } from '@openinula/reactivity-parser'; +import ElementGenerator from '../HelperGenerators/ElementGenerator'; +import { typeNode } from '../shard'; +import { InulaNodeType } from '@openinula/next-shared'; + +export default class ExpGenerator extends ElementGenerator { + run() { + let { content } = this.viewParticle as ExpParticle; + content = this.alterPropView(content)!; + + const nodeName = this.generateNodeName(); + + this.addInitStatement(this.declareExpNode(nodeName, content.value, content.dependenciesNode)); + + if (content.depMask) { + this.addUpdateStatements(content.depMask, this.updateExpNode(nodeName, content.value, content.dependenciesNode)); + } + + return nodeName; + } + + /** + * @View + * ${nodeName} = createNode(InulaNodeType.Exp, () => ${value}, dependenciesNode) + */ + private declareExpNode(nodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement { + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.identifier(nodeName), + this.t.callExpression(this.t.identifier(this.importMap.createNode), [ + typeNode(InulaNodeType.Exp), + this.t.arrowFunctionExpression([], value), + dependenciesNode ?? this.t.nullLiteral(), + ]) + ) + ); + } + + /** + * @View + * updateNode(${nodeName}, value, dependenciesNode) + */ + private updateExpNode(nodeName: string, value: t.Expression, dependenciesNode: t.ArrayExpression): t.Statement { + return this.optionalExpression( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.updateNode), [ + this.t.identifier(nodeName), + this.t.arrowFunctionExpression([], value), + dependenciesNode ?? this.t.nullLiteral(), + ]) + ); + } +} diff --git a/packages/transpiler/view-generator/src/NodeGenerators/ForGenerator.ts b/packages/transpiler/view-generator/src/NodeGenerators/ForGenerator.ts index ae198ec4a5773e52a2ad47aefc9299633a027f12..fba10014e439c3d7785e0820b06eede4e0b6b85d 100644 --- a/packages/transpiler/view-generator/src/NodeGenerators/ForGenerator.ts +++ b/packages/transpiler/view-generator/src/NodeGenerators/ForGenerator.ts @@ -1,132 +1,127 @@ -import { type types as t } from '@babel/core'; -import BaseGenerator from '../HelperGenerators/BaseGenerator'; -import { type ForParticle, type ViewParticle } from '@openinula/reactivity-parser'; - -export default class ForGenerator extends BaseGenerator { - run() { - const { item, array, key, children, index } = this.viewParticle as ForParticle; - - const dlNodeName = this.generateNodeName(); - - // ---- Declare for node - this.addInitStatement(this.declareForNode(dlNodeName, array.value, item, index, children, array.depMask ?? 0, key)); - - // ---- Update statements - this.addUpdateStatements(array.depMask, this.updateForNode(dlNodeName, array.value, item, key)); - this.addUpdateStatementsWithoutDep(this.updateForNodeItem(dlNodeName)); - - return dlNodeName; - } - - /** - * @View - * ${dlNodeName} = new ForNode(${array}, ${depNum}, ${array}.map(${item} => ${key}), - * ((${item}, $updateArr, $idx) => { - * $updateArr[$idx] = (changed, $item) => { - * ${item} = $item - * {$updateStatements} - * } - * ${children} - * return [...${topLevelNodes}] - * }) - */ - private declareForNode( - dlNodeName: string, - array: t.Expression, - item: t.LVal, - index: t.Identifier | null, - children: ViewParticle[], - depNum: number, - key: t.Expression - ): t.Statement { - // ---- NodeFunc - const [childStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren(children, false, true); - const idxId = index ?? this.t.identifier('$idx'); - // ---- Update func - childStatements.unshift( - ...this.declareNodes(nodeIdx), - this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.memberExpression(this.t.identifier('$updateArr'), idxId, true), - this.t.arrowFunctionExpression( - [...this.updateParams, this.t.identifier('$item')], - this.t.blockStatement([ - this.t.expressionStatement(this.t.assignmentExpression('=', item, this.t.identifier('$item'))), - ...this.geneUpdateBody(updateStatements).body, - ]) - ) - ) - ) - ); - - // ---- Return statement - childStatements.push(this.generateReturnStatement(topLevelNodes)); - - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.identifier(dlNodeName), - this.t.newExpression(this.t.identifier(this.importMap.ForNode), [ - array, - this.t.numericLiteral(depNum), - this.getForKeyStatement(array, item, key, index), - this.t.arrowFunctionExpression( - [item as any, idxId, this.t.identifier('$updateArr')], - this.t.blockStatement(childStatements) - ), - ]) - ) - ); - } - - /** - * @View - * ${array}.map(${item, index} => ${key}) - */ - private getForKeyStatement( - array: t.Expression, - item: t.LVal, - key: t.Expression, - index: t.Identifier | null - ): t.Expression { - const params = [item as any]; - if (index) { - params.push(index); - } - return this.t.isNullLiteral(key) - ? key - : this.t.callExpression(this.t.memberExpression(array, this.t.identifier('map')), [ - this.t.arrowFunctionExpression(params, key), - ]); - } - - /** - * @View - * ${dlNodeName}.updateArray(${array}, ${array}.map(${item} => ${key})) - */ - private updateForNode(dlNodeName: string, array: t.Expression, item: t.LVal, key: t.Expression): t.Statement { - return this.optionalExpression( - dlNodeName, - this.t.callExpression(this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('updateArray')), [ - array, - ...this.updateParams.slice(1), - this.getForKeyStatement(array, item, key), - ]) - ); - } - - /** - * @View - * ${dlNodeName}?.update(changed) - */ - private updateForNodeItem(dlNodeName: string): t.Statement { - return this.optionalExpression( - dlNodeName, - this.t.callExpression( - this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('update')), - this.updateParams - ) - ); - } -} +import { type types as t } from '@babel/core'; +import BaseGenerator from '../HelperGenerators/BaseGenerator'; +import { type ForParticle, type ViewParticle } from '@openinula/reactivity-parser'; +import { typeNode } from '../shard'; +import { InulaNodeType } from '@openinula/next-shared'; + +export default class ForGenerator extends BaseGenerator { + run() { + const { item, array, key, children, index } = this.viewParticle as ForParticle; + + const nodeName = this.generateNodeName(); + + // ---- Declare for node + this.addInitStatement(this.declareForNode(nodeName, array.value, item, index, children, array.depMask ?? 0, key)); + + // ---- Update statements + this.addUpdateStatements(array.depMask, this.updateForNode(nodeName, array.value, item, key)); + this.addUpdateStatementsWithoutDep(this.updateForNodeItem(nodeName)); + + return nodeName; + } + + /** + * @View + * ${nodeName} = createNode(InulaNodeType.For, ${array}, ${depNum}, ${array}.map(${item} => ${key}), + * ((${item}, $updateArr, $idx) => { + * $updateArr[$idx] = (changed, $item) => { + * ${item} = $item + * {$updateStatements} + * } + * ${children} + * return [...${topLevelNodes}] + * }) + */ + private declareForNode( + nodeName: string, + array: t.Expression, + item: t.LVal, + index: t.Identifier | null, + children: ViewParticle[], + depNum: number, + key: t.Expression + ): t.Statement { + // ---- NodeFunc + const [childStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren(children, false, true); + const idxId = index ?? this.t.identifier('$idx'); + // ---- Update func + childStatements.unshift( + ...this.declareNodes(nodeIdx), + this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.memberExpression(this.t.identifier('$updateArr'), idxId, true), + this.t.arrowFunctionExpression( + [...this.updateParams, this.t.identifier('$item')], + this.t.blockStatement([ + this.t.expressionStatement(this.t.assignmentExpression('=', item, this.t.identifier('$item'))), + ...this.geneUpdateBody(updateStatements).body, + ]) + ) + ) + ) + ); + + // ---- Return statement + childStatements.push(this.generateReturnStatement(topLevelNodes)); + + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.identifier(nodeName), + this.t.callExpression(this.t.identifier(this.importMap.createNode), [ + typeNode(InulaNodeType.For), + array, + this.t.numericLiteral(depNum), + this.getForKeyStatement(array, item, key), + this.t.arrowFunctionExpression( + [item as any, idxId, this.t.identifier('$updateArr')], + this.t.blockStatement(childStatements) + ), + ]) + ) + ); + } + + /** + * @View + * ${array}.map(${item} => ${key}) + */ + private getForKeyStatement(array: t.Expression, item: t.LVal, key: t.Expression): t.Expression { + return this.t.isNullLiteral(key) + ? key + : this.t.callExpression(this.t.memberExpression(array, this.t.identifier('map')), [ + this.t.arrowFunctionExpression([item as any], key), + ]); + } + + /** + * @View + * updateNode(${nodeName}, ${array}, ${array}.map(${item} => ${key})) + */ + private updateForNode(nodeName: string, array: t.Expression, item: t.LVal, key: t.Expression): t.Statement { + return this.optionalExpression( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.updateNode), [ + this.t.identifier(nodeName), + array, + ...this.updateParams.slice(1), + this.getForKeyStatement(array, item, key), + ]) + ); + } + + /** + * @View + * updateChildren(${nodeName}, changed) + */ + private updateForNodeItem(nodeName: string): t.Statement { + return this.optionalExpression( + nodeName, + this.t.callExpression(this.t.identifier(this.importMap.updateChildren), [ + this.t.identifier(nodeName), + ...this.updateParams, + ]) + ); + } +} diff --git a/packages/transpiler/view-generator/src/NodeGenerators/HTMLGenerator.ts b/packages/transpiler/view-generator/src/NodeGenerators/HTMLGenerator.ts index 5441fe45df5328cb534c461f1529c1258d687588..26db94e36aed1dd1ee9e4d86aad8bbd10400b79c 100644 --- a/packages/transpiler/view-generator/src/NodeGenerators/HTMLGenerator.ts +++ b/packages/transpiler/view-generator/src/NodeGenerators/HTMLGenerator.ts @@ -1,89 +1,89 @@ -import { type types as t } from '@babel/core'; -import { Bitmap, type HTMLParticle } from '@openinula/reactivity-parser'; -import HTMLPropGenerator from '../HelperGenerators/HTMLPropGenerator'; - -export default class HTMLGenerator extends HTMLPropGenerator { - run() { - const { tag, props, children } = this.viewParticle as HTMLParticle; - - const dlNodeName = this.generateNodeName(); - - this.addInitStatement(this.declareHTMLNode(dlNodeName, tag)); - - // ---- Resolve props - // ---- Use the tag name to check if the prop is internal for the tag, - // for dynamic tag, we can't check it, so we just assume it's not internal - // represent by the "ANY" tag name - const tagName = this.t.isStringLiteral(tag) ? tag.value : 'ANY'; - let allDepMask: Bitmap = 0; - Object.entries(props).forEach(([key, { value, depMask, dependenciesNode }]) => { - if (key === 'didUpdate') return; - if (depMask) { - allDepMask |= depMask; - } - this.addInitStatement(this.addHTMLProp(dlNodeName, tagName, key, value, depMask, dependenciesNode)); - }); - if (props.didUpdate) { - this.addUpdateStatements(allDepMask, this.addOnUpdate(dlNodeName, props.didUpdate.value)); - } - - // ---- Resolve children - const childNames: string[] = []; - let mutable = false; - children.forEach((child, idx) => { - const [initStatements, childName] = this.generateChild(child); - childNames.push(childName); - this.addInitStatement(...initStatements); - if (child.type === 'html' || child.type === 'text') { - this.addInitStatement(this.appendChild(dlNodeName, childName)); - } else { - mutable = true; - this.addInitStatement(this.insertNode(dlNodeName, childName, idx)); - } - }); - if (mutable) this.addInitStatement(this.setHTMLNodes(dlNodeName, childNames)); - - return dlNodeName; - } - - /** - * @View - * ${dlNodeName} = createElement(${tag}) - */ - private declareHTMLNode(dlNodeName: string, tag: t.Expression): t.Statement { - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.identifier(dlNodeName), - this.t.callExpression(this.t.identifier(this.importMap.createElement), [tag]) - ) - ); - } - - /** - * @View - * ${dlNodeName}._$nodes = [...${childNames}] - */ - private setHTMLNodes(dlNodeName: string, childNames: string[]): t.Statement { - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.memberExpression(this.t.identifier(dlNodeName), this.t.identifier('_$nodes')), - this.t.arrayExpression(childNames.map(name => this.t.identifier(name))) - ) - ); - } - - /** - * @View - * appendNode(${dlNodeName}, ${childNodeName}) - */ - private appendChild(dlNodeName: string, childNodeName: string): t.Statement { - return this.t.expressionStatement( - this.t.callExpression(this.t.identifier(this.importMap.appendNode), [ - this.t.identifier(dlNodeName), - this.t.identifier(childNodeName), - ]) - ); - } -} +import { type types as t } from '@babel/core'; +import { Bitmap, type HTMLParticle } from '@openinula/reactivity-parser'; +import HTMLPropGenerator from '../HelperGenerators/HTMLPropGenerator'; + +export default class HTMLGenerator extends HTMLPropGenerator { + run() { + const { tag, props, children } = this.viewParticle as HTMLParticle; + + const nodeName = this.generateNodeName(); + + this.addInitStatement(this.declareHTMLNode(nodeName, tag)); + + // ---- Resolve props + // ---- Use the tag name to check if the prop is internal for the tag, + // for dynamic tag, we can't check it, so we just assume it's not internal + // represent by the "ANY" tag name + const tagName = this.t.isStringLiteral(tag) ? tag.value : 'ANY'; + let allDepMask: Bitmap = 0; + Object.entries(props).forEach(([key, { value, depMask, dependenciesNode }]) => { + if (key === 'didUpdate') return; + if (depMask) { + allDepMask |= depMask; + } + this.addInitStatement(this.addHTMLProp(nodeName, tagName, key, value, depMask, dependenciesNode)); + }); + if (props.didUpdate) { + this.addUpdateStatements(allDepMask, this.addOnUpdate(nodeName, props.didUpdate.value)); + } + + // ---- Resolve children + const childNames: string[] = []; + let mutable = false; + children.forEach((child, idx) => { + const [initStatements, childName] = this.generateChild(child); + childNames.push(childName); + this.addInitStatement(...initStatements); + if (child.type === 'html' || child.type === 'text') { + this.addInitStatement(this.appendChild(nodeName, childName)); + } else { + mutable = true; + this.addInitStatement(this.insertNode(nodeName, childName, idx)); + } + }); + if (mutable) this.addInitStatement(this.setHTMLNodes(nodeName, childNames)); + + return nodeName; + } + + /** + * @View + * ${dlNodeName} = createElement(${tag}) + */ + private declareHTMLNode(dlNodeName: string, tag: t.Expression): t.Statement { + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.identifier(dlNodeName), + this.t.callExpression(this.t.identifier(this.importMap.createElement), [tag]) + ) + ); + } + + /** + * @View + * ${nodeName}._$nodes = [...${childNames}] + */ + private setHTMLNodes(nodeName: string, childNames: string[]): t.Statement { + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.memberExpression(this.t.identifier(nodeName), this.t.identifier('_$nodes')), + this.t.arrayExpression(childNames.map(name => this.t.identifier(name))) + ) + ); + } + + /** + * @View + * appendNode(${nodeName}, ${childNodeName}) + */ + private appendChild(nodeName: string, childNodeName: string): t.Statement { + return this.t.expressionStatement( + this.t.callExpression(this.t.identifier(this.importMap.appendNode), [ + this.t.identifier(nodeName), + this.t.identifier(childNodeName), + ]) + ); + } +} diff --git a/packages/transpiler/view-generator/src/NodeGenerators/IfGenerator.ts b/packages/transpiler/view-generator/src/NodeGenerators/IfGenerator.ts index b9490debd5b10881246da2c5497c209a0371c135..595f445ef1d872243f429bb1687e8946d33401a1 100644 --- a/packages/transpiler/view-generator/src/NodeGenerators/IfGenerator.ts +++ b/packages/transpiler/view-generator/src/NodeGenerators/IfGenerator.ts @@ -1,98 +1,98 @@ -import { type types as t } from '@babel/core'; -import { type IfParticle, type IfBranch, Bitmap } from '@openinula/reactivity-parser'; -import CondGenerator from '../HelperGenerators/CondGenerator'; - -export default class IfGenerator extends CondGenerator { - run() { - const { branches } = this.viewParticle as IfParticle; - const deps = branches.reduce((acc, { condition }) => { - return condition.depMask ? acc | condition.depMask : acc; - }, 0); - - // ---- declareIfNode - const dlNodeName = this.generateNodeName(); - this.addInitStatement(this.declareIfNode(dlNodeName, branches, deps)); - - this.addUpdateStatements(deps, this.updateCondNodeCond(dlNodeName)); - this.addUpdateStatementsWithoutDep(this.updateCondNode(dlNodeName)); - - return dlNodeName; - } - - /** - * @View - * if (${test}) { ${body} } else { ${alternate} } - */ - geneIfStatement(test: t.Expression, body: t.Statement[], alternate: t.Statement): t.IfStatement { - return this.t.ifStatement(test, this.t.blockStatement(body), alternate); - } - - /** - * @View - * const ${dlNodeName} = new IfNode(($thisCond) => { - * if (cond1) { - * if ($thisCond.cond === 0) return - * ${children} - * $thisCond.cond = 0 - * return [nodes] - * } else if (cond2) { - * if ($thisCond.cond === 1) return - * ${children} - * $thisCond.cond = 1 - * return [nodes] - * } - * }) - */ - private declareIfNode(dlNodeName: string, branches: IfBranch[], depMask: Bitmap): t.Statement { - // ---- If no else statement, add one - if ( - !this.t.isBooleanLiteral(branches[branches.length - 1].condition.value, { - value: true, - }) - ) { - branches.push({ - condition: { - value: this.t.booleanLiteral(true), - depMask: 0, - allDepBits: [], - dependenciesNode: this.t.arrayExpression([]), - }, - children: [], - }); - } - const ifStatement = branches.reverse().reduce((acc, { condition, children }, i) => { - const idx = branches.length - i - 1; - // ---- Generate children - const [childStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren(children, false, true); - - // ---- Even if no updateStatements, we still need reassign an empty updateFunc - // to overwrite the previous one - /** - * $thisCond.updateFunc = (changed) => { ${updateStatements} } - */ - const updateNode = this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('updateFunc')), - this.t.arrowFunctionExpression(this.updateParams, this.geneUpdateBody(updateStatements)) - ) - ); - - // ---- Update func - childStatements.unshift(...this.declareNodes(nodeIdx), updateNode); - - // ---- Check cond and update cond - childStatements.unshift(this.geneCondCheck(idx), this.geneCondIdx(idx)); - - // ---- Return statement - childStatements.push(this.geneCondReturnStatement(topLevelNodes, idx)); - - // ---- else statement - if (i === 0) return this.t.blockStatement(childStatements); - - return this.geneIfStatement(condition.value, childStatements, acc); - }, undefined); - - return this.declareCondNode(dlNodeName, this.t.blockStatement([ifStatement]), depMask); - } -} +import { type types as t } from '@babel/core'; +import { type IfParticle, type IfBranch, Bitmap } from '@openinula/reactivity-parser'; +import CondGenerator from '../HelperGenerators/CondGenerator'; + +export default class IfGenerator extends CondGenerator { + run() { + const { branches } = this.viewParticle as IfParticle; + const deps = branches.reduce((acc, { condition }) => { + return condition.depMask ? acc | condition.depMask : acc; + }, 0); + + // ---- declareIfNode + const nodeName = this.generateNodeName(); + this.addInitStatement(this.declareIfNode(nodeName, branches, deps)); + + this.addUpdateStatements(deps, this.updateCondNodeCond(nodeName)); + this.addUpdateStatementsWithoutDep(this.updateCondNode(nodeName)); + + return nodeName; + } + + /** + * @View + * if (${test}) { ${body} } else { ${alternate} } + */ + geneIfStatement(test: t.Expression, body: t.Statement[], alternate: t.Statement): t.IfStatement { + return this.t.ifStatement(test, this.t.blockStatement(body), alternate); + } + + /** + * @View + * const ${nodeName} = new IfNode(($thisCond) => { + * if (cond1) { + * if ($thisCond.cond === 0) return + * ${children} + * $thisCond.cond = 0 + * return [nodes] + * } else if (cond2) { + * if ($thisCond.cond === 1) return + * ${children} + * $thisCond.cond = 1 + * return [nodes] + * } + * }) + */ + private declareIfNode(nodeName: string, branches: IfBranch[], depMask: Bitmap): t.Statement { + // ---- If no else statement, add one + if ( + !this.t.isBooleanLiteral(branches[branches.length - 1].condition.value, { + value: true, + }) + ) { + branches.push({ + condition: { + value: this.t.booleanLiteral(true), + depMask: 0, + allDepBits: [], + dependenciesNode: this.t.arrayExpression([]), + }, + children: [], + }); + } + const ifStatement = branches.reverse().reduce((acc, { condition, children }, i) => { + const idx = branches.length - i - 1; + // ---- Generate children + const [childStatements, topLevelNodes, updateStatements, nodeIdx] = this.generateChildren(children, false, true); + + // ---- Even if no updateStatements, we still need reassign an empty updateFunc + // to overwrite the previous one + /** + * $thisCond.updateFunc = (changed) => { ${updateStatements} } + */ + const updateNode = this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.memberExpression(this.t.identifier('$thisCond'), this.t.identifier('updateFunc')), + this.t.arrowFunctionExpression(this.updateParams, this.geneUpdateBody(updateStatements)) + ) + ); + + // ---- Update func + childStatements.unshift(...this.declareNodes(nodeIdx), updateNode); + + // ---- Check cond and update cond + childStatements.unshift(this.geneCondCheck(idx), this.geneCondIdx(idx)); + + // ---- Return statement + childStatements.push(this.geneCondReturnStatement(topLevelNodes, idx)); + + // ---- else statement + if (i === 0) return this.t.blockStatement(childStatements); + + return this.geneIfStatement(condition.value, childStatements, acc); + }, undefined); + + return this.declareCondNode(nodeName, this.t.blockStatement([ifStatement]), depMask); + } +} diff --git a/packages/transpiler/view-generator/src/NodeGenerators/TemplateGenerator.ts b/packages/transpiler/view-generator/src/NodeGenerators/TemplateGenerator.ts index 9ccf55fa3ce20739157235e8161b4642dbafad9b..70ad7713c6500f4e525daf83e4ad841394258f4f 100644 --- a/packages/transpiler/view-generator/src/NodeGenerators/TemplateGenerator.ts +++ b/packages/transpiler/view-generator/src/NodeGenerators/TemplateGenerator.ts @@ -1,278 +1,278 @@ -import { type types as t } from '@babel/core'; -import { Bitmap, type HTMLParticle, type TemplateParticle } from '@openinula/reactivity-parser'; -import HTMLPropGenerator from '../HelperGenerators/HTMLPropGenerator'; - -export default class TemplateGenerator extends HTMLPropGenerator { - run() { - const { template, mutableParticles, props } = this.viewParticle as TemplateParticle; - - const dlNodeName = this.generateNodeName(); - // ---- Add template declaration to class - const templateName = this.genTemplate(template); - // ---- Declare template node in View body - this.addInitStatement(this.declareTemplateNode(dlNodeName, templateName)); - - // ---- Insert elements first - const paths: number[][] = []; - props.forEach(({ path }) => { - paths.push(path); - }); - mutableParticles.forEach(({ path }) => { - paths.push(path.slice(0, -1)); - }); - const [insertElementStatements, pathNameMap] = this.insertElements(paths, dlNodeName); - this.addInitStatement(...insertElementStatements); - - // ---- Resolve props - const didUpdateMap: Record = {}; - props.forEach(({ tag, path, key, value, depMask, dependenciesNode }) => { - const name = pathNameMap[path.join('.')]; - if (!didUpdateMap[name]) - didUpdateMap[name] = { - depMask: 0, - }; - if (key === 'didUpdate') { - didUpdateMap[name].value = value; - return; - } - - didUpdateMap[name].depMask |= depMask ?? 0; - - this.addInitStatement(this.addHTMLProp(name, tag, key, value, depMask, dependenciesNode)); - }); - - Object.entries(didUpdateMap).forEach(([name, { depMask, value }]) => { - if (!value) return; - this.addUpdateStatements(depMask, this.addOnUpdate(name, value)); - }); - - // ---- Resolve mutable particles - mutableParticles.forEach(particle => { - const path = particle.path; - // ---- Find parent htmlElement - const parentName = pathNameMap[path.slice(0, -1).join('.')]; - const [initStatements, childName] = this.generateChild(particle); - this.addInitStatement(...initStatements); - this.addInitStatement(this.insertNode(parentName, childName, path[path.length - 1])); - }); - - return dlNodeName; - } - - /** - * @View - * const ${templateName} = (() => { - * let _$node0, _$node1, ... - * ${template} - * - * return _$node0 - * })() - */ - private genTemplate(template: HTMLParticle): string { - const templateName = this.generateTemplateName(); - const [statements, nodeName, , nodeIdx] = this.generateChild(template, false, true); - this.addTemplate( - templateName, - this.t.callExpression( - this.t.arrowFunctionExpression( - [], - this.t.blockStatement([ - ...this.declareNodes(nodeIdx), - ...statements, - this.t.returnStatement(this.t.identifier(nodeName)), - ]) - ), - [] - ) - ); - - return templateName; - } - - /** - * @View - * ${dlNodeName} = ${templateName}.cloneNode(true) - */ - private declareTemplateNode(dlNodeName: string, templateName: string): t.Statement { - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.identifier(dlNodeName), - this.t.callExpression( - this.t.memberExpression(this.t.identifier(templateName), this.t.identifier('cloneNode')), - [this.t.booleanLiteral(true)] - ) - ) - ); - } - - /** - * @View - * ${dlNodeName}.firstChild - * or - * ${dlNodeName}.firstChild.nextSibling - * or - * ... - * ${dlNodeName}.childNodes[${num}] - */ - private insertElement(dlNodeName: string, path: number[], offset: number): t.Statement { - const newNodeName = this.generateNodeName(); - if (path.length === 0) { - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.identifier(newNodeName), - Array.from({ length: offset }).reduce((acc: t.Expression) => { - return this.t.memberExpression(acc as t.Expression, this.t.identifier('nextSibling')); - }, this.t.identifier(dlNodeName)) - ) - ); - } - const addFirstChild = (object: t.Expression) => - // ---- ${object}.firstChild - this.t.memberExpression(object, this.t.identifier('firstChild')); - const addSecondChild = (object: t.Expression) => - // ---- ${object}.firstChild.nextSibling - this.t.memberExpression(addFirstChild(object), this.t.identifier('nextSibling')); - const addThirdChild = (object: t.Expression) => - // ---- ${object}.firstChild.nextSibling.nextSibling - this.t.memberExpression(addSecondChild(object), this.t.identifier('nextSibling')); - const addOtherChild = (object: t.Expression, num: number) => - // ---- ${object}.childNodes[${num}] - this.t.memberExpression( - this.t.memberExpression(object, this.t.identifier('childNodes')), - this.t.numericLiteral(num), - true - ); - const addNextSibling = (object: t.Expression) => - // ---- ${object}.nextSibling - this.t.memberExpression(object, this.t.identifier('nextSibling')); - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.identifier(newNodeName), - path.reduce((acc: t.Expression, cur: number, idx) => { - if (idx === 0 && offset > 0) { - for (let i = 0; i < offset; i++) acc = addNextSibling(acc); - } - if (cur === 0) return addFirstChild(acc); - if (cur === 1) return addSecondChild(acc); - if (cur === 2) return addThirdChild(acc); - return addOtherChild(acc, cur); - }, this.t.identifier(dlNodeName)) - ) - ); - } - - /** - * @brief Insert elements to the template node from the paths - * @param paths - * @param dlNodeName - * @returns - */ - private insertElements(paths: number[][], dlNodeName: string): [t.Statement[], Record] { - const [statements, collect] = HTMLPropGenerator.statementsCollector(); - const nameMap: Record = { [dlNodeName]: [] }; - - const commonPrefixPaths = TemplateGenerator.pathWithCommonPrefix(paths); - - commonPrefixPaths.forEach(path => { - const res = TemplateGenerator.findBestNodeAndPath(nameMap, path, dlNodeName); - const [, pat, offset] = res; - let name = res[0]; - - if (pat.length !== 0 || offset !== 0) { - collect(this.insertElement(name, pat, offset)); - name = this.generateNodeName(this.nodeIdx); - nameMap[name] = path; - } - }); - const pathNameMap = Object.fromEntries(Object.entries(nameMap).map(([name, path]) => [path.join('.'), name])); - - return [statements, pathNameMap]; - } - - // ---- Path related - /** - * @brief Extract common prefix from paths - * e.g. - * [0, 1, 2, 3] + [0, 1, 2, 4] => [0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 4] - * [0, 1, 2] is the common prefix - * @param paths - * @returns paths with common prefix - */ - private static pathWithCommonPrefix(paths: number[][]): number[][] { - const allPaths = [...paths]; - paths.forEach(path0 => { - paths.forEach(path1 => { - if (path0 === path1) return; - for (let i = 0; i < path0.length; i++) { - if (path0[i] !== path1[i]) { - if (i !== 0) { - allPaths.push(path0.slice(0, i)); - } - break; - } - } - }); - }); - - // ---- Sort by length and then by first element, small to large - const sortedPaths = allPaths.sort((a, b) => { - if (a.length !== b.length) return a.length - b.length; - return a[0] - b[0]; - }); - - // ---- Deduplicate - const deduplicatedPaths = [...new Set(sortedPaths.map(path => path.join('.')))].map(path => - path.split('.').filter(Boolean).map(Number) - ); - - return deduplicatedPaths; - } - - /** - * @brief Find the best node name and path for the given path by looking into the nameMap. - * If there's a full match, return the name and an empty path - * If there's a partly match, return the name and the remaining path - * If there's a nextSibling match, return the name and the remaining path with sibling offset - * @param nameMap - * @param path - * @param defaultName - * @returns [name, path, siblingOffset] - */ - private static findBestNodeAndPath( - nameMap: Record, - path: number[], - defaultName: string - ): [string, number[], number] { - let bestMatchCount = 0; - let bestMatchName: string | undefined; - let bestHalfMatch: [string, number, number] | undefined; - Object.entries(nameMap).forEach(([name, pat]) => { - let matchCount = 0; - const pathLength = pat.length; - for (let i = 0; i < pathLength; i++) { - if (pat[i] === path[i]) matchCount++; - } - if (matchCount === pathLength - 1) { - const offset = path[pathLength - 1] - pat[pathLength - 1]; - if (offset > 0 && offset <= 3) { - bestHalfMatch = [name, matchCount, offset]; - } - } - if (matchCount !== pat.length) return; - if (matchCount > bestMatchCount) { - bestMatchName = name; - bestMatchCount = matchCount; - } - }); - if (!bestMatchName) { - if (bestHalfMatch) { - return [bestHalfMatch[0], path.slice(bestHalfMatch[1] + 1), bestHalfMatch[2]]; - } - return [defaultName, path, 0]; - } - return [bestMatchName, path.slice(bestMatchCount), 0]; - } -} +import { type types as t } from '@babel/core'; +import { Bitmap, type HTMLParticle, type TemplateParticle } from '@openinula/reactivity-parser'; +import HTMLPropGenerator from '../HelperGenerators/HTMLPropGenerator'; + +export default class TemplateGenerator extends HTMLPropGenerator { + run() { + const { template, mutableParticles, props } = this.viewParticle as TemplateParticle; + + const nodeName = this.generateNodeName(); + // ---- Add template declaration to class + const templateName = this.genTemplate(template); + // ---- Declare template node in View body + this.addInitStatement(this.declareTemplateNode(nodeName, templateName)); + + // ---- Insert elements first + const paths: number[][] = []; + props.forEach(({ path }) => { + paths.push(path); + }); + mutableParticles.forEach(({ path }) => { + paths.push(path.slice(0, -1)); + }); + const [insertElementStatements, pathNameMap] = this.insertElements(paths, nodeName); + this.addInitStatement(...insertElementStatements); + + // ---- Resolve props + const didUpdateMap: Record = {}; + props.forEach(({ tag, path, key, value, depMask, dependenciesNode }) => { + const name = pathNameMap[path.join('.')]; + if (!didUpdateMap[name]) + didUpdateMap[name] = { + depMask: 0, + }; + if (key === 'didUpdate') { + didUpdateMap[name].value = value; + return; + } + + didUpdateMap[name].depMask |= depMask ?? 0; + + this.addInitStatement(this.addHTMLProp(name, tag, key, value, depMask, dependenciesNode)); + }); + + Object.entries(didUpdateMap).forEach(([name, { depMask, value }]) => { + if (!value) return; + this.addUpdateStatements(depMask, this.addOnUpdate(name, value)); + }); + + // ---- Resolve mutable particles + mutableParticles.forEach(particle => { + const path = particle.path; + // ---- Find parent htmlElement + const parentName = pathNameMap[path.slice(0, -1).join('.')]; + const [initStatements, childName] = this.generateChild(particle); + this.addInitStatement(...initStatements); + this.addInitStatement(this.insertNode(parentName, childName, path[path.length - 1])); + }); + + return nodeName; + } + + /** + * @View + * const ${templateName} = (() => { + * let _$node0, _$node1, ... + * ${template} + * + * return _$node0 + * })() + */ + private genTemplate(template: HTMLParticle): string { + const templateName = this.generateTemplateName(); + const [statements, nodeName, , nodeIdx] = this.generateChild(template, false, true); + this.addTemplate( + templateName, + this.t.callExpression( + this.t.arrowFunctionExpression( + [], + this.t.blockStatement([ + ...this.declareNodes(nodeIdx), + ...statements, + this.t.returnStatement(this.t.identifier(nodeName)), + ]) + ), + [] + ) + ); + + return templateName; + } + + /** + * @View + * ${nodeName} = ${templateName}.cloneNode(true) + */ + private declareTemplateNode(nodeName: string, templateName: string): t.Statement { + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.identifier(nodeName), + this.t.callExpression( + this.t.memberExpression(this.t.identifier(templateName), this.t.identifier('cloneNode')), + [this.t.booleanLiteral(true)] + ) + ) + ); + } + + /** + * @View + * ${nodeName}.firstChild + * or + * ${nodeName}.firstChild.nextSibling + * or + * ... + * ${nodeName}.childNodes[${num}] + */ + private insertElement(nodeName: string, path: number[], offset: number): t.Statement { + const newNodeName = this.generateNodeName(); + if (path.length === 0) { + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.identifier(newNodeName), + Array.from({ length: offset }).reduce((acc: t.Expression) => { + return this.t.memberExpression(acc as t.Expression, this.t.identifier('nextSibling')); + }, this.t.identifier(nodeName)) + ) + ); + } + const addFirstChild = (object: t.Expression) => + // ---- ${object}.firstChild + this.t.memberExpression(object, this.t.identifier('firstChild')); + const addSecondChild = (object: t.Expression) => + // ---- ${object}.firstChild.nextSibling + this.t.memberExpression(addFirstChild(object), this.t.identifier('nextSibling')); + const addThirdChild = (object: t.Expression) => + // ---- ${object}.firstChild.nextSibling.nextSibling + this.t.memberExpression(addSecondChild(object), this.t.identifier('nextSibling')); + const addOtherChild = (object: t.Expression, num: number) => + // ---- ${object}.childNodes[${num}] + this.t.memberExpression( + this.t.memberExpression(object, this.t.identifier('childNodes')), + this.t.numericLiteral(num), + true + ); + const addNextSibling = (object: t.Expression) => + // ---- ${object}.nextSibling + this.t.memberExpression(object, this.t.identifier('nextSibling')); + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.identifier(newNodeName), + path.reduce((acc: t.Expression, cur: number, idx) => { + if (idx === 0 && offset > 0) { + for (let i = 0; i < offset; i++) acc = addNextSibling(acc); + } + if (cur === 0) return addFirstChild(acc); + if (cur === 1) return addSecondChild(acc); + if (cur === 2) return addThirdChild(acc); + return addOtherChild(acc, cur); + }, this.t.identifier(nodeName)) + ) + ); + } + + /** + * @brief Insert elements to the template node from the paths + * @param paths + * @param nodeName + * @returns + */ + private insertElements(paths: number[][], nodeName: string): [t.Statement[], Record] { + const [statements, collect] = HTMLPropGenerator.statementsCollector(); + const nameMap: Record = { [nodeName]: [] }; + + const commonPrefixPaths = TemplateGenerator.pathWithCommonPrefix(paths); + + commonPrefixPaths.forEach(path => { + const res = TemplateGenerator.findBestNodeAndPath(nameMap, path, nodeName); + const [, pat, offset] = res; + let name = res[0]; + + if (pat.length !== 0 || offset !== 0) { + collect(this.insertElement(name, pat, offset)); + name = this.generateNodeName(this.nodeIdx); + nameMap[name] = path; + } + }); + const pathNameMap = Object.fromEntries(Object.entries(nameMap).map(([name, path]) => [path.join('.'), name])); + + return [statements, pathNameMap]; + } + + // ---- Path related + /** + * @brief Extract common prefix from paths + * e.g. + * [0, 1, 2, 3] + [0, 1, 2, 4] => [0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 4] + * [0, 1, 2] is the common prefix + * @param paths + * @returns paths with common prefix + */ + private static pathWithCommonPrefix(paths: number[][]): number[][] { + const allPaths = [...paths]; + paths.forEach(path0 => { + paths.forEach(path1 => { + if (path0 === path1) return; + for (let i = 0; i < path0.length; i++) { + if (path0[i] !== path1[i]) { + if (i !== 0) { + allPaths.push(path0.slice(0, i)); + } + break; + } + } + }); + }); + + // ---- Sort by length and then by first element, small to large + const sortedPaths = allPaths.sort((a, b) => { + if (a.length !== b.length) return a.length - b.length; + return a[0] - b[0]; + }); + + // ---- Deduplicate + const deduplicatedPaths = [...new Set(sortedPaths.map(path => path.join('.')))].map(path => + path.split('.').filter(Boolean).map(Number) + ); + + return deduplicatedPaths; + } + + /** + * @brief Find the best node name and path for the given path by looking into the nameMap. + * If there's a full match, return the name and an empty path + * If there's a partly match, return the name and the remaining path + * If there's a nextSibling match, return the name and the remaining path with sibling offset + * @param nameMap + * @param path + * @param defaultName + * @returns [name, path, siblingOffset] + */ + private static findBestNodeAndPath( + nameMap: Record, + path: number[], + defaultName: string + ): [string, number[], number] { + let bestMatchCount = 0; + let bestMatchName: string | undefined; + let bestHalfMatch: [string, number, number] | undefined; + Object.entries(nameMap).forEach(([name, pat]) => { + let matchCount = 0; + const pathLength = pat.length; + for (let i = 0; i < pathLength; i++) { + if (pat[i] === path[i]) matchCount++; + } + if (matchCount === pathLength - 1) { + const offset = path[pathLength - 1] - pat[pathLength - 1]; + if (offset > 0 && offset <= 3) { + bestHalfMatch = [name, matchCount, offset]; + } + } + if (matchCount !== pat.length) return; + if (matchCount > bestMatchCount) { + bestMatchName = name; + bestMatchCount = matchCount; + } + }); + if (!bestMatchName) { + if (bestHalfMatch) { + return [bestHalfMatch[0], path.slice(bestHalfMatch[1] + 1), bestHalfMatch[2]]; + } + return [defaultName, path, 0]; + } + return [bestMatchName, path.slice(bestMatchCount), 0]; + } +} diff --git a/packages/transpiler/view-generator/src/NodeGenerators/TextGenerator.ts b/packages/transpiler/view-generator/src/NodeGenerators/TextGenerator.ts index 69a26fd15b9f868719f5a9db10b1561358d1b74a..6cf58d3030a6d82c158f55c654a40593f4d36612 100644 --- a/packages/transpiler/view-generator/src/NodeGenerators/TextGenerator.ts +++ b/packages/transpiler/view-generator/src/NodeGenerators/TextGenerator.ts @@ -1,54 +1,51 @@ -import { type types as t } from '@babel/core'; -import { type TextParticle } from '@openinula/reactivity-parser'; -import BaseGenerator from '../HelperGenerators/BaseGenerator'; - -export default class TextGenerator extends BaseGenerator { - run() { - const { content } = this.viewParticle as TextParticle; - - const dlNodeName = this.generateNodeName(); - - this.addInitStatement(this.declareTextNode(dlNodeName, content.value, content.dependenciesNode)); - - if (content.depMask) { - this.addUpdateStatements( - content.depMask, - this.updateTextNode(dlNodeName, content.value, content.dependenciesNode) - ); - } - - return dlNodeName; - } - - /** - * @View - * ${dlNodeName} = createTextNode(${value}, ${deps}) - */ - private declareTextNode(dlNodeName: string, value: t.Expression, dependenciesNode: t.Expression): t.Statement { - return this.t.expressionStatement( - this.t.assignmentExpression( - '=', - this.t.identifier(dlNodeName), - this.t.callExpression(this.t.identifier(this.importMap.createTextNode), [value, dependenciesNode]) - ) - ); - } - - /** - * @View - * ${dlNodeName} && updateText(${dlNodeName}, () => ${value}, ${deps}) - */ - private updateTextNode(dlNodeName: string, value: t.Expression, dependenciesNode: t.Expression): t.Statement { - return this.t.expressionStatement( - this.t.logicalExpression( - '&&', - this.t.identifier(dlNodeName), - this.t.callExpression(this.t.identifier(this.importMap.updateText), [ - this.t.identifier(dlNodeName), - this.t.arrowFunctionExpression([], value), - dependenciesNode, - ]) - ) - ); - } -} +import { type types as t } from '@babel/core'; +import { type TextParticle } from '@openinula/reactivity-parser'; +import BaseGenerator from '../HelperGenerators/BaseGenerator'; + +export default class TextGenerator extends BaseGenerator { + run() { + const { content } = this.viewParticle as TextParticle; + + const nodeName = this.generateNodeName(); + + this.addInitStatement(this.declareTextNode(nodeName, content.value, content.dependenciesNode)); + + if (content.depMask) { + this.addUpdateStatements(content.depMask, this.updateTextNode(nodeName, content.value, content.dependenciesNode)); + } + + return nodeName; + } + + /** + * @View + * ${nodeName} = createTextNode(${value}, ${deps}) + */ + private declareTextNode(nodeName: string, value: t.Expression, dependenciesNode: t.Expression): t.Statement { + return this.t.expressionStatement( + this.t.assignmentExpression( + '=', + this.t.identifier(nodeName), + this.t.callExpression(this.t.identifier(this.importMap.createTextNode), [value, dependenciesNode]) + ) + ); + } + + /** + * @View + * ${nodeName} && updateText(${nodeName}, () => ${value}, ${deps}) + */ + private updateTextNode(nodeName: string, value: t.Expression, dependenciesNode: t.Expression): t.Statement { + return this.t.expressionStatement( + this.t.logicalExpression( + '&&', + this.t.identifier(nodeName), + this.t.callExpression(this.t.identifier(this.importMap.updateText), [ + this.t.identifier(nodeName), + this.t.arrowFunctionExpression([], value), + dependenciesNode, + ]) + ) + ); + } +} diff --git a/packages/transpiler/view-generator/src/ViewGenerator.ts b/packages/transpiler/view-generator/src/ViewGenerator.ts index 5e79f9cda694a02caa6e2a5b413e34f6fef2b3b4..ebfa0a58c7b170a2e4f427786f7a5d3f4bd41cff 100644 --- a/packages/transpiler/view-generator/src/ViewGenerator.ts +++ b/packages/transpiler/view-generator/src/ViewGenerator.ts @@ -1,113 +1,113 @@ -import { type types as t } from '@babel/core'; -import { type ViewParticle } from '@openinula/reactivity-parser'; -import { type ViewGeneratorConfig } from './types'; -import BaseGenerator, { prefixMap } from './HelperGenerators/BaseGenerator'; -import CompGenerator from './NodeGenerators/CompGenerator'; -import HTMLGenerator from './NodeGenerators/HTMLGenerator'; -import TemplateGenerator from './NodeGenerators/TemplateGenerator'; -import ForGenerator from './NodeGenerators/ForGenerator'; -import IfGenerator from './NodeGenerators/IfGenerator'; -import ContextGenerator from './NodeGenerators/ContextGenerator'; -import TextGenerator from './NodeGenerators/TextGenerator'; -import ExpGenerator from './NodeGenerators/ExpGenerator'; - -export default class ViewGenerator { - config: ViewGeneratorConfig; - t: typeof t; - - /** - * @brief Construct the view generator from config - * @param config - */ - constructor(config: ViewGeneratorConfig) { - this.config = config; - this.t = config.babelApi.types; - this.templateIdx = config.templateIdx; - } - - /** - * @brief Different generator classes for different view particle types - */ - static generatorMap: Record = { - comp: CompGenerator, - html: HTMLGenerator, - template: TemplateGenerator, - for: ForGenerator, - if: IfGenerator, - context: ContextGenerator, - text: TextGenerator, - exp: ExpGenerator, - }; - - /** - * @brief Generate the view given the view particles, mainly used for child particles parsing - * @param viewParticles - * @returns [initStatements, updateStatements, classProperties, topLevelNodes] - */ - generateChildren( - viewParticles: ViewParticle[] - ): [t.Statement[], Record, t.VariableDeclaration[], string[]] { - const allInitStatements: t.Statement[] = []; - const allTemplates: t.VariableDeclaration[] = []; - const allUpdateStatements: Record = {}; - const topLevelNodes: string[] = []; - - viewParticles.forEach(viewParticle => { - const [initStatements, updateStatements, templates, nodeName] = this.generateChild(viewParticle); - allInitStatements.push(...initStatements); - Object.entries(updateStatements).forEach(([depNum, statements]) => { - if (!allUpdateStatements[Number(depNum)]) { - allUpdateStatements[Number(depNum)] = []; - } - allUpdateStatements[Number(depNum)].push(...statements); - }); - allTemplates.push(...templates); - topLevelNodes.push(nodeName); - }); - - return [allInitStatements, allUpdateStatements, allTemplates, topLevelNodes]; - } - - nodeIdx = -1; - templateIdx = -1; - - /** - * @brief Generate the view given the view particle, using generator from the map - * @param viewParticle - * @returns - */ - generateChild(viewParticle: ViewParticle) { - const { type } = viewParticle; - const GeneratorClass = ViewGenerator.generatorMap[type]; - if (!GeneratorClass) { - throw new Error(`Unknown view particle type: ${type}`); - } - const generator = new GeneratorClass(viewParticle, this.config); - generator.nodeIdx = this.nodeIdx; - generator.templateIdx = this.templateIdx; - const result = generator.generate(); - this.nodeIdx = generator.nodeIdx; - this.templateIdx = generator.templateIdx; - return result; - } - - /** - * @View - * let node1, node2, ... - */ - declareNodes(): t.Statement[] { - if (this.nodeIdx === -1) return []; - return [ - this.t.variableDeclaration( - 'let', - Array.from({ length: this.nodeIdx + 1 }, (_, i) => - this.t.variableDeclarator(this.t.identifier(`${prefixMap.node}${i}`)) - ) - ), - ]; - } - - get updateParams() { - return [this.t.identifier('$changed')]; - } -} +import { type types as t } from '@babel/core'; +import { type ViewParticle } from '@openinula/reactivity-parser'; +import { type ViewGeneratorConfig } from './types'; +import BaseGenerator, { prefixMap } from './HelperGenerators/BaseGenerator'; +import CompGenerator from './NodeGenerators/CompGenerator'; +import HTMLGenerator from './NodeGenerators/HTMLGenerator'; +import TemplateGenerator from './NodeGenerators/TemplateGenerator'; +import ForGenerator from './NodeGenerators/ForGenerator'; +import IfGenerator from './NodeGenerators/IfGenerator'; +import ContextGenerator from './NodeGenerators/ContextGenerator'; +import TextGenerator from './NodeGenerators/TextGenerator'; +import ExpGenerator from './NodeGenerators/ExpGenerator'; + +export default class ViewGenerator { + config: ViewGeneratorConfig; + t: typeof t; + + /** + * @brief Construct the view generator from config + * @param config + */ + constructor(config: ViewGeneratorConfig) { + this.config = config; + this.t = config.babelApi.types; + this.templateIdx = config.templateIdx; + } + + /** + * @brief Different generator classes for different view particle types + */ + static generatorMap: Record = { + comp: CompGenerator, + html: HTMLGenerator, + template: TemplateGenerator, + for: ForGenerator, + if: IfGenerator, + context: ContextGenerator, + text: TextGenerator, + exp: ExpGenerator, + }; + + /** + * @brief Generate the view given the view particles, mainly used for child particles parsing + * @param viewParticles + * @returns [initStatements, updateStatements, classProperties, topLevelNodes] + */ + generateChildren( + viewParticles: ViewParticle[] + ): [t.Statement[], Record, t.VariableDeclaration[], string[]] { + const allInitStatements: t.Statement[] = []; + const allTemplates: t.VariableDeclaration[] = []; + const allUpdateStatements: Record = {}; + const topLevelNodes: string[] = []; + + viewParticles.forEach(viewParticle => { + const [initStatements, updateStatements, templates, nodeName] = this.generateChild(viewParticle); + allInitStatements.push(...initStatements); + Object.entries(updateStatements).forEach(([depNum, statements]) => { + if (!allUpdateStatements[Number(depNum)]) { + allUpdateStatements[Number(depNum)] = []; + } + allUpdateStatements[Number(depNum)].push(...statements); + }); + allTemplates.push(...templates); + topLevelNodes.push(nodeName); + }); + + return [allInitStatements, allUpdateStatements, allTemplates, topLevelNodes]; + } + + nodeIdx = -1; + templateIdx = -1; + + /** + * @brief Generate the view given the view particle, using generator from the map + * @param viewParticle + * @returns + */ + generateChild(viewParticle: ViewParticle) { + const { type } = viewParticle; + const GeneratorClass = ViewGenerator.generatorMap[type]; + if (!GeneratorClass) { + throw new Error(`Unknown view particle type: ${type}`); + } + const generator = new GeneratorClass(viewParticle, this.config); + generator.nodeIdx = this.nodeIdx; + generator.templateIdx = this.templateIdx; + const result = generator.generate(); + this.nodeIdx = generator.nodeIdx; + this.templateIdx = generator.templateIdx; + return result; + } + + /** + * @View + * let node1, node2, ... + */ + declareNodes(): t.Statement[] { + if (this.nodeIdx === -1) return []; + return [ + this.t.variableDeclaration( + 'let', + Array.from({ length: this.nodeIdx + 1 }, (_, i) => + this.t.variableDeclarator(this.t.identifier(`${prefixMap.node}${i}`)) + ) + ), + ]; + } + + get updateParams() { + return [this.t.identifier('$changed')]; + } +} diff --git a/packages/transpiler/view-generator/src/error.ts b/packages/transpiler/view-generator/src/error.ts index e40b20a840c7df8a73245fe9df7aa96a680c1035..efe25f302b04454056f916ee92d96b991e15092c 100644 --- a/packages/transpiler/view-generator/src/error.ts +++ b/packages/transpiler/view-generator/src/error.ts @@ -1,14 +1,14 @@ -import { createErrorHandler } from '@openinula/error-handler'; - -export const DLError = createErrorHandler( - 'ViewGenerator', - { - 1: 'Element prop in HTML should be a function or an identifier', - 2: 'Unrecognized HTML common prop', - 3: 'Do prop only accepts function or arrow function', - }, - {}, - { - 1: 'ExpressionNode only supports prop as element and lifecycle, receiving $0', - } -); +import { createErrorHandler } from '@openinula/error-handler'; + +export const DLError = createErrorHandler( + 'ViewGenerator', + { + 1: 'Element prop in HTML should be a function or an identifier', + 2: 'Unrecognized HTML common prop', + 3: 'Do prop only accepts function or arrow function', + }, + {}, + { + 1: 'ExpressionNode only supports prop as element and lifecycle, receiving $0', + } +); diff --git a/packages/transpiler/view-generator/src/index.ts b/packages/transpiler/view-generator/src/index.ts index 19f511089af3467e3d7bb0844946e00ec6d5e57b..694e73f53c2514cb73fe3b056a70a0e885c6b08e 100644 --- a/packages/transpiler/view-generator/src/index.ts +++ b/packages/transpiler/view-generator/src/index.ts @@ -1,13 +1,13 @@ -import { type ViewParticle } from '@openinula/reactivity-parser'; -import { type ViewGeneratorConfig } from './types'; -import MainViewGenerator from './MainViewGenerator'; -import type { types as t } from '@babel/core'; - -export function generateView( - viewParticles: ViewParticle[], - config: ViewGeneratorConfig -): [t.ArrowFunctionExpression | null, t.Statement[], t.Statement[], t.ArrayExpression] { - return new MainViewGenerator(config).generate(viewParticles); -} - -export * from './types'; +import { type ViewParticle } from '@openinula/reactivity-parser'; +import { type ViewGeneratorConfig } from './types'; +import MainViewGenerator from './MainViewGenerator'; +import type { types as t } from '@babel/core'; + +export function generateView( + viewParticles: ViewParticle[], + config: ViewGeneratorConfig +): [t.ArrowFunctionExpression | null, t.Statement[], t.Statement[], t.ArrayExpression] { + return new MainViewGenerator(config).generate(viewParticles); +} + +export * from './types'; diff --git a/packages/transpiler/view-generator/src/shard.ts b/packages/transpiler/view-generator/src/shard.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a8db27ab59b358709611d66836bff5b5f69e4ba --- /dev/null +++ b/packages/transpiler/view-generator/src/shard.ts @@ -0,0 +1,8 @@ +import { getTypeName, InulaNodeType } from '@openinula/next-shared'; +import { types as t } from '@openinula/babel-api'; + +export function typeNode(type: InulaNodeType) { + const node = t.numericLiteral(type); + t.addComment(node, 'trailing', getTypeName(type), false); + return node; +} diff --git a/packages/transpiler/view-generator/src/types.ts b/packages/transpiler/view-generator/src/types.ts index 8534bd92492f8591c938c49464031eb84c787922..27d97a23b8657893aaae563a1beebfd01c78d201 100644 --- a/packages/transpiler/view-generator/src/types.ts +++ b/packages/transpiler/view-generator/src/types.ts @@ -1,15 +1,15 @@ -import type Babel from '@babel/core'; - -export interface ViewGeneratorConfig { - babelApi: typeof Babel; - importMap: Record; - templateIdx: number; - attributeMap: Record; - alterAttributeMap: Record; - /** - * The subComponent in comp, [name, usedBit] - */ - subComps: Array<[string, number]>; - genTemplateKey: (key: string) => string; - wrapUpdate: (node: Babel.types.Statement | Babel.types.Expression | null) => void; -} +import type Babel from '@babel/core'; + +export interface ViewGeneratorConfig { + babelApi: typeof Babel; + importMap: Record; + templateIdx: number; + attributeMap: Record; + alterAttributeMap: Record; + /** + * The subComponent in comp, [name, usedBit] + */ + subComps: Array<[string, number]>; + genTemplateKey: (key: string) => string; + wrapUpdate: (node: Babel.types.Statement | Babel.types.Expression | null) => void; +} diff --git a/packages/transpiler/view-generator/tsconfig.json b/packages/transpiler/view-generator/tsconfig.json index e0932d7828a8e6b8f5b2c7752a24057e6461adf0..c003315788d1e0451295fc2f3330f040cbeb5b00 100644 --- a/packages/transpiler/view-generator/tsconfig.json +++ b/packages/transpiler/view-generator/tsconfig.json @@ -1,13 +1,13 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", - "strict": true, - "esModuleInterop": true - }, - "ts-node": { - "esm": true - } +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true + }, + "ts-node": { + "esm": true + } } \ No newline at end of file diff --git a/packages/transpiler/vite-plugin-inula-next/CHANGELOG.md b/packages/transpiler/vite-plugin-inula-next/CHANGELOG.md index b7ffd1e249e7ebb268a2d8d3ee7d2e95c9cedabb..d091a32fdcdc2a5b1e01a4acc50b538427a8a1ff 100644 --- a/packages/transpiler/vite-plugin-inula-next/CHANGELOG.md +++ b/packages/transpiler/vite-plugin-inula-next/CHANGELOG.md @@ -1,14 +1,22 @@ -# vite-plugin-inula-next - -## 0.0.3 - -### Patch Changes - -- babel-preset-inula-next@0.0.3 - -## 0.0.2 - -### Patch Changes - -- Updated dependencies [2f9d373] - - babel-preset-inula-next@0.0.2 +# vite-plugin-inula-next + +## 0.0.4 + +### Patch Changes + +- support hook +- Updated dependencies + - babel-preset-inula-next@0.0.4 + +## 0.0.3 + +### Patch Changes + +- babel-preset-inula-next@0.0.3 + +## 0.0.2 + +### Patch Changes + +- Updated dependencies [2f9d373] + - babel-preset-inula-next@0.0.2 diff --git a/packages/transpiler/vite-plugin-inula-next/src/index.ts b/packages/transpiler/vite-plugin-inula-next/src/index.ts index 4d6ce4f4f8dcd982a567e824b438eae12b7d1279..e571fcc29f08cd8477ef61bcccd5885073d2ad59 100644 --- a/packages/transpiler/vite-plugin-inula-next/src/index.ts +++ b/packages/transpiler/vite-plugin-inula-next/src/index.ts @@ -1,46 +1,46 @@ -import { transform } from '@babel/core'; -import InulaNext, { type InulaNextOption } from '@openinula/babel-preset-inula-next'; -import { minimatch } from 'minimatch'; -import { Plugin, TransformResult } from 'vite'; - -export default function (options: Partial = {}): Plugin { - const { - files: preFiles = '**/*.{js,jsx,ts,tsx}', - excludeFiles: preExcludeFiles = '**/{dist,node_modules,lib}/*.{js,ts}', - } = options; - const files = Array.isArray(preFiles) ? preFiles : [preFiles]; - const excludeFiles = Array.isArray(preExcludeFiles) ? preExcludeFiles : [preExcludeFiles]; - - return { - name: 'Inula-Next', - enforce: 'pre', - transform(code: string, id: string) { - let enter = false; - for (const allowedPath of files) { - if (minimatch(id, allowedPath)) { - enter = true; - break; - } - } - for (const notAllowedPath of excludeFiles) { - if (minimatch(id, notAllowedPath)) { - enter = false; - break; - } - } - if (!enter) return; - try { - return transform(code, { - babelrc: false, - configFile: false, - presets: [[InulaNext, options]], - sourceMaps: true, - filename: id, - }) as TransformResult; - } catch (err) { - console.error('Error:', err); - } - return ''; - }, - }; -} +import { transform } from '@babel/core'; +import InulaNext, { type InulaNextOption } from '@openinula/babel-preset-inula-next'; +import { minimatch } from 'minimatch'; +import { Plugin, TransformResult } from 'vite'; + +export default function (options: Partial = {}): Plugin { + const { + files: preFiles = '**/*.{js,jsx,ts,tsx}', + excludeFiles: preExcludeFiles = '**/{dist,node_modules,lib}/*.{js,ts}', + } = options; + const files = Array.isArray(preFiles) ? preFiles : [preFiles]; + const excludeFiles = Array.isArray(preExcludeFiles) ? preExcludeFiles : [preExcludeFiles]; + + return { + name: 'Inula-Next', + enforce: 'pre', + transform(code: string, id: string) { + let enter = false; + for (const allowedPath of files) { + if (minimatch(id, allowedPath)) { + enter = true; + break; + } + } + for (const notAllowedPath of excludeFiles) { + if (minimatch(id, notAllowedPath)) { + enter = false; + break; + } + } + if (!enter) return; + try { + return transform(code, { + babelrc: false, + configFile: false, + presets: [[InulaNext, options]], + sourceMaps: true, + filename: id, + }) as TransformResult; + } catch (err) { + console.error('Error:', err); + } + return ''; + }, + }; +} diff --git a/packages/transpiler/vite-plugin-inula-next/tsconfig.json b/packages/transpiler/vite-plugin-inula-next/tsconfig.json index e0932d7828a8e6b8f5b2c7752a24057e6461adf0..c003315788d1e0451295fc2f3330f040cbeb5b00 100644 --- a/packages/transpiler/vite-plugin-inula-next/tsconfig.json +++ b/packages/transpiler/vite-plugin-inula-next/tsconfig.json @@ -1,13 +1,13 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", - "strict": true, - "esModuleInterop": true - }, - "ts-node": { - "esm": true - } +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true + }, + "ts-node": { + "esm": true + } } \ No newline at end of file