diff --git a/.changeset/README.md b/.changeset/README.md
new file mode 100644
index 00000000..e5b6d8d6
--- /dev/null
+++ b/.changeset/README.md
@@ -0,0 +1,8 @@
+# Changesets
+
+Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
+with multi-package repos, or single-package repos to help you version and publish your code. You can
+find the full documentation for it [in our repository](https://github.com/changesets/changesets)
+
+We have a quick list of common questions to get you started engaging with this project in
+[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
diff --git a/.changeset/config.json b/.changeset/config.json
new file mode 100644
index 00000000..4369ffef
--- /dev/null
+++ b/.changeset/config.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
+ "changelog": "@changesets/cli/changelog",
+ "commit": false,
+ "fixed": [],
+ "linked": [],
+ "access": "restricted",
+ "baseBranch": "master",
+ "updateInternalDependencies": "patch",
+ "ignore": ["create-inula", "openinula", "inula-cli", "inula-dev-tools", "inula-intl", "inula-request", "inula-router", "inula-vite-app", "inula-webpack-app"]
+}
diff --git a/.gitignore b/.gitignore
index 8636e699..ba7855d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,11 @@
-/node_modules
+node_modules
.idea
.vscode
package-lock.json
pnpm-lock.yaml
-/packages/**/node_modules
/packages/inula-cli/lib
build
/packages/inula-router/connectRouter
/packages/inula-router/router
+dist
+.history
\ No newline at end of file
diff --git a/api-2.1-fn.md b/api-2.1-fn.md
new file mode 100644
index 00000000..8e584688
--- /dev/null
+++ b/api-2.1-fn.md
@@ -0,0 +1,55 @@
+```js
+function MyComp({prop1}) {
+ let count = 1
+ let doubleCount = count * 2
+
+ const updateCount = () => {
+ count++
+ }
+
+ return
{prop1}
;
+}
+```
+
+```js
+function MyComp() {
+ let prop1$$prop
+
+ let count = 1
+ let doubleCount = count * 2
+ let update$$doubleCount = () => {
+ if (cached()) return
+ doubleCount = count * 2
+ }
+
+ const updateCount = () => {
+ viewModel.update(count++)
+ }
+
+ // ----
+ const node = createElement('div')
+ setProps(node, "textContent", [prop1])
+
+ const viewModel = Inula.createElement({
+ node,
+ updateProp: (propName, value) => {
+ if (propName === 'prop1') {
+ prop1$$prop = value
+ }
+ },
+ updateState: (changed) => {
+ if (changed & 0x1) {
+ update$$doubleCount()
+ }
+ },
+ updateView: (changed) => {
+ if (changed & 0x1) {
+ setProps(node, "textContent", [prop1$$prop])
+ }
+ }
+ })
+
+ return viewModel
+}
+```
+
diff --git a/api-2.1.md b/api-2.1.md
new file mode 100644
index 00000000..b5c51ac2
--- /dev/null
+++ b/api-2.1.md
@@ -0,0 +1,2029 @@
+# Identify a Component/hook
+## 1. Auto detection config turned on in Babel
+```json
+{
+ "autoNamingDetection": true
+}
+```
+Will follow the following rules:
+* For Components:
+ 1. Inside a .jsx file
+ 2. Function/ArrowFunction Named as PascalCase
+ ```jsx
+ // ✅
+ function MyComp() {
+ return (
+
+
Hello
+
+ )
+ }
+ // ❌
+ function foo() {
+ return (
+
+
Hello
+
+ )
+ }
+ ```
+* For hooks:
+ 1. Function/ArrowFunction starts with "use"
+ ```jsx
+ // ✅
+ function useMyHook() {
+ return 1
+ }
+ // ❌
+ function foo() {
+ return 2
+ ```
+## 2. Manual detection
+No naming convention is required. The user will have to manually wrap the function with `Component` or `Hook` function.
+
+* For Components, wrapped with `Component` function
+```jsx
+const myComp = Component(function() {
+ return (
+
+
Hello
+
+ )
+})
+```
+* For hooks, wrapped with `Hook` function
+```jsx
+const myHook = Hook(function() {
+ return 1
+})
+```
+
+
+# States/Computed
+```jsx
+function MyComp() {
+ let a, b
+ let count = 100
+ let doubleCount = count * 2
+ let [a1, a2, a3] = getArray()
+ ...
+}
+
+```
+
+```jsx
+class MyComp extends View {
+ // Multiple declarations in one line
+ a
+ b
+ // Single declaration
+ count = 100
+ // Computed states
+ doubleCount = this.count * 2
+
+ // Deconstructing assignment
+ a1
+ a2
+ a3
+ // Using @Watch to deconstruct
+ @Watch
+ _$deconstruct_$id$() {
+ [this.a1, this.a2, this.a3] = getArray()
+ }
+}
+```
+
+# Props
+
+## Step 1: Collect props/destructured/restProps
+### Deconstructed in the function signature
+```jsx
+function MyComp({ prop1, prop2: [p20, p21], ...otherProps }) {
+ ...
+}
+```
+Collected as:
+```js
+props = ["prop1", "prop2"]
+destructured = [
+ ["prop2": astOf("[p20, p21]")],
+]
+restProps = "otherProps"
+```
+### Deconstructing assignment / Direct usage in the function body
+#### 1. Deconstructing assignment
+```jsx
+function MyComp(props) {
+ const { prop1, prop2: [p20, p21], ...otherProps } = props
+ ...
+}
+```
+Collected as:
+```js
+props = ["prop1", "prop2"]
+destructured = [
+ ["prop2", astOf("[p20, p21]")],
+]
+restProps = "otherProps"
+```
+After:
+* Delete this statement
+
+#### 2. Direct usage
+```jsx
+function MyComp(props) {
+ let prop1 = props.prop1
+ let [p20, p21] = props.prop2
+ // Not just assignment, but also any other type of usage
+ console.log(props.prop3)
+ ...
+}
+```
+collected as:
+```js
+props = ["prop1", "prop2", "prop3"]
+destructured = [] // The reason why it's empty is that the deconstructed props are treated like Computed States
+restProps = null
+```
+After:
+* Replace all `props.propName` with `propName`
+
+Goals of these two "After"s:
+* Make sure no `props` is used in the function body
+
+## Step2: Generate Props in class
+With the collected props/destructured/restProps:
+```jsx
+props = ["prop1", "prop2"]
+destructured = [
+ ["prop2", astOf("[p20, p21]")],
+]
+restProps = "otherProps"
+```
+Generate:
+```jsx
+class MyComp extends View {
+ // Props with @Prop decorator
+ @Prop prop1
+ @Prop prop2
+
+ // Destructured props, same logic as Computed States
+ p20
+ p21
+ @Watch
+ _$deconstruct_$id$() {
+ [this.p20, this.p21] = this.prop2
+ }
+
+ // Rest props
+ @RestProps otherProps
+}
+```
+
+## Things to note
+1. Multiple deconstructed statements are allowed:
+```jsx
+function MyComp(props) {
+ const { prop1, prop2: [p20, p21] } = props
+ const { prop1: p1, prop2: [p20A, p21A] } = props
+ ...
+}
+```
+will be collected as:
+```js
+props = ["prop1", "prop2"]
+destructured = [
+ ["prop2", astOf("[p20, p21]")],
+ ["prop2", astOf("[p20A, p21A)")]
+] // this is why destructured is an array of arrays instead of an object
+```
+and be generated into:
+```jsx
+class MyComp extends View {
+ @Prop prop1
+ @Prop prop2
+
+ p20
+ p21
+ @Watch
+ _$deconstruct_$id1$() {
+ [this.p20, this.p21] = this.prop2
+ }
+
+ p20A
+ p21A
+ @Watch
+ _$deconstruct_$id2$() {
+ [this.p20A, this.p21A] = this.prop2
+ }
+}
+```
+
+2. Multiple restProps are NOT allowed:
+```jsx
+function MyComp(props) {
+ const { prop1, prop2, ...otherProps1 } = props
+ const { prop2, prop3, ...otherProps2 } = props
+ ...
+}
+```
+Will throw an error because this will confuse the compiler what props are explicitly used and what are collected as restProps.
+
+3. Anywhere that `props` is used in the function body will be replaced with the prop name directly:
+```jsx
+function MyComp(props) {
+ let p1 = props.prop1
+ console.log(props.prop2)
+ console.log(props.prop3.xx[0].yy)
+ ...
+}
+```
+will be replaced as:
+```jsx
+class MyComp extends View {
+ @Prop prop1
+ @Prop prop2
+ @Prop prop3
+
+ p1 = this.prop1
+
+ willMount() {
+ console.log(this.prop2)
+ console.log(this.prop3.xx[0].yy)
+ }
+}
+```
+
+# For loops
+`for` tag basic usage (just like solid's For):
+```jsx
+function MyComp() {
+ let arr = [1, 2, 3]
+ return (
+ {(item) =>
+
)}
+ >
+ )
+}
+```
+when it's detected as a `map` function and the returned value is a JSX, it will be converted into a `for` tag(for better performance):
+```jsx
+function MyComp() {
+ let arr = [1, 2, 3]
+ return (
+ {(item) =>
+
{item}
+ }
+ )
+}
+```
+NOTE: only detect the last map function:
+```jsx
+function MyComp() {
+ let arr = [1, 2, 3]
+ return (
+ <>
+ {
+ Object.values(arr)
+ .map(item =>
+ }
+ )
+}
+```
+
+### 2. Children
+1. Simple returned value (no local variables). Children will be treated as regular elements/components
+```jsx
+function MyComp() {
+ return (
+ {(item) =>
+
{item}
+ }
+ )
+}
+```
+children collected in the parser will only be:
+```jsx
+
{item}
+```
+
+2. With local variables. Transformed into a new Component:
+```jsx
+function MyComp() {
+ return (
+ {(info) => {
+ const { item } = info
+ return
{item}
+ }}
+ )
+}
+```
+will be converted into:
+```jsx
+function MyComp() {
+ function Comp_$id$({ info }) {
+ const { item } = info
+ return
{item}
+ }
+
+ return (
+ {(info) => (
+
+ )}
+ )
+}
+```
+which creates a new component for the children and converts the children into the first situation(1. Simple returned value)
+
+## Key prop
+Just like React, we can add a `key` prop to first level children of the `for` tag:
+```jsx
+function MyComp() {
+ let arr = [1, 2, 3]
+ return (
+ {(item) =>
+
{item}
+ }
+ )
+}
+```
+
+
+# JSX
+JSX is tricky because we need to allow different types of JSX to be used in the function body.
+
+Let's consider a piece of JSX:
+```jsx
+function MyComp() {
+ let count = 0
+ let jsxSlice =
{count}
+ ...
+}
+```
+We have different options:
+1. jsx -> array of nodes
+
+we can use an IIFE to convert the JSX into an array of nodes:
+```jsx
+class MyComp extends View {
+ count = 0
+ jsxSlice = (() => {
+ const node0 = createElement("div")
+ node0.textContent = this.count
+ return node0
+ })()
+}
+```
+This looks good but when `count` changes, the whole `jsxSlice` will be re-calculated, which means elements will be re-created. This is not good for performance.
+
+So instead of using an IIFE, we can first extract the JSX into a nested component:
+```jsx
+function MyComp() {
+ let count = 0
+ function Comp_$id$() {
+ return
{count}
+ }
+ let jsxSlice =
+ ...
+}
+```
+In this case, no matter how many times `count` changes, the `jsxSlice` will not be re-calculated, we only need to call the update function of the `Comp_$id$` component.
+
+So we'll convert this type of JSX into a nested component:
+```jsx
+class MyComp extends View {
+ count = 0
+ // Use an IIFE to return a class because we need to forward the parent's this. X in _$thisX represents the level of nesting
+ // Also it needs to be static(not a state variable)
+ @Static Comp_$id$ = (() => {
+ const _$this0 = this
+ return class extends View {
+ willUnmount() {
+ // Remove all parent level when unmount
+ clear(_$this0)
+ }
+ Body() {
+ const node0 = createElement("div")
+ const node1 = new ExpressionNode(_$this0.count)
+ appendChild(node0, [node1])
+
+ const update = changed => {
+ if (changed & 0x0001) {
+ node1.update(_$this0.count)
+ }
+ }
+ // Because we need to confine the changed scope, we put update into all parent level's updates
+ this._$updates.add(update) // current level
+ _$this0._$updates.add(update) // parent level
+ // _$this1._$updates.add(update) // if there's a third level
+ return node0
+ }
+ }
+ })()
+
+ jsxSlice = new this.Comp_$id$()
+}
+```
+
+A more complex example:
+```jsx
+function MyComp() {
+ let count = 100
+ const jsxSlice =
{count}
+ const jsxArray = [
1
,
{count}
]
+ function jsxFunc() {
+ // This is a function that returns JSX
+ // because the function name is smallCamelCased
+ return
+ }
+ const jsxArray = [, ]
+ function jsxFunc() {
+ function Comp_$id4$() {
+ return
{count}
+ }
+ // This is a function that returns JSX
+ // because the function name is smallCamelCased
+ return
+ }
+ function InternalComp({ doubleCount }) {
+ return (
+
+ {count /* This is Parent's state */}
+ {doubleCount /* This is current's prop */}
+
+ )
+ }
+
+ return (
+
+ {jsxSlice}
+ {jsxArray}
+ {jsxFunc()}
+
+
+ )
+}
+```
+will be converted into:
+```jsx
+class MyComp extends View {
+ count = 100
+ // $$count = 0x0001 // Indicate count is the first state
+
+ @Static Comp_$id1$ = (() => {
+ const _$this0 = this
+ return class extends View {
+ willUnmount() {
+ // Remove all parent level when unmount
+ clear(_$this0)
+ }
+ Body() {
+ const node0 = createElement("div")
+ const node1 = new ExpressionNode(_$this0.count)
+ appendChild(node0, [node1])
+ const update = changed => {
+ if (changed & 0x0001) {
+ node1.update(_$this0.count)
+ }
+ }
+ this._$updates.add(update)
+ _$this0._$updates.add(update)
+ return node0
+ }
+ }
+ })()
+ jsxSlice = new this.Comp_$id$(null, [...this._$scopes, this])
+
+ @Static Comp_$id2$ = class extends View {
+ Body() {
+ const node0 = createElement("div")
+ node0.textContent = "1"
+ return node0
+ }
+ }
+ Comp_$id3$ = /*same as Comp_$id1$*/
+ jsxArray = [new this.Comp_$id2$(), new this.Comp_$id3$()]
+
+ jsxFunc() {
+ const Comp_$id4$ = /*same as Comp_$id1$*/
+ return new Comp_$id4$()
+ }
+ @Static InternalComp = (() => {
+ const _$this0 = this
+ return class extends View {
+ @Prop doubleCount
+ // $$doubleCount = 0x0010 // Index will be INHERITED from the parent!
+ willUnmount() {
+ // Remove all parent level when unmount
+ clear(_$this0)
+ }
+ Body() {
+ const node1 = createElement("div")
+ const node2 = new ExpressionNode(_$this0.count)
+ const node3 = new ExpressionNode(this.doubleCount)
+ appendChild(node1, [node2, node3])
+ const update = changed => {
+ if (changed & 0x0001) {
+ node2.update(_$this0.count)
+ }
+ if (changed & 0x0010) {
+ node3.update(this.doubleCount)
+ }
+ }
+ this._$updates.add(update)
+ _$this0._$updates.add(update)
+ return [node1]
+ }
+ }
+ })()
+
+ Body() {
+ const node0 = createElement("div")
+ const node1 = new ExpressionNode(this.jsxSlice)
+ const node2 = new ExpressionNode(this.jsxArray)
+ const node3 = new ExpressionNode(this.jsxFunc())
+ const node4 = new this.InternalComp({
+ doubleCount: this.count * 2
+ })
+ // second argument is the parent scopes, use this to access parent's states/variables
+
+ appendChild(node0, [node1, node2, node3, node4])
+ const update = changed => {
+ if (changed & 0x0001) {
+ // _$this0.jsxFunc will be re-called
+ node3.update(this.jsxFunc(this.count))
+ // doubleCount prop will be updated due to count change
+ node4.updateProp("doubleCount", this.count * 2)
+ }
+ }
+ this._$updates.add(update)
+ return node0
+ }
+}
+```
+
+# Early return
+Early return is also tricky....
+
+```jsx
+function MyComp() {
+ let count = 100
+ if (count === 100) {
+ return
100
+ }
+ let flag = true
+ return
Not 100 {flag}
+}
+```
+it will be converted in a functional level:
+```jsx
+function MyComp() {
+ let count = 100
+ function Comp_$id1$() {
+ return
100
+ }
+ function Comp_$id2$() {
+ let flag = true
+ return
Not 100 {flag}
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+```
+and then be converted into class using the same jsx logic as the previous section.
+
+A more complex example:
+```jsx
+function MyComp() {
+ let count = 100
+ if (count === 100) {
+ return
100
+ }
+ let flag = true
+ if (flag) {
+ let cc = 0
+ if (cc === 200) {
+ return
cc is 200
+ } else if (cc === 100) {
+ return
cc is 100
+ }
+ return
Flag is true
+ }
+ return
Not 100 {flag}
+}
+```
+will be converted into:
+```jsx
+function MyComp() {
+ let count = 100
+ function Comp_$id1$() {
+ return
100
+ }
+ function Comp_$id2$() {
+ let flag = true
+ function Comp_$id3$() {
+ let cc = 0
+ function Comp_$id4$() {
+ return
)
+ }
+}
+```
+Basically turn `useMyHook(props)` into a property of `use(useMyHook, props)._$return`. The logic for props handling is the same as the Component.
+
+NOTE: We have a constraint here that we only accept `an object of props` like we do in the `Component` function.
+
+Another example:
+```jsx
+function useMyHook() {
+ let count = 100
+ let doubleCount = count * 2
+
+ return { count, doubleCount }
+}
+```
+will be converted into:
+```jsx
+class MyHook extends Model {
+ count = 100
+ doubleCount = this.count * 2
+
+ _$return = { count: this.count, doubleCount: this.doubleCount }
+}
+```
+
+used in a component:
+```jsx
+function MyComp() {
+ const { count, doubleCount } = useMyHook()
+ return
+ );
+}
+
+render(App, document.getElementById('main'));
diff --git a/demos/v2/src/example/attributeBinding.jsx b/demos/v2/src/example/attributeBinding.jsx
new file mode 100644
index 00000000..d4aec129
--- /dev/null
+++ b/demos/v2/src/example/attributeBinding.jsx
@@ -0,0 +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 { render } from '@openinula/next';
+function AttributeBinding() {
+ let color = 'green';
+ function changeColor() {
+ color = color === 'red' ? 'green' : 'red';
+ }
+
+ return (
+
+ Click me to change color.
+
+ );
+}
+
+render(AttributeBinding, document.getElementById('app'));
diff --git a/demos/v2/src/example/condition.jsx b/demos/v2/src/example/condition.jsx
new file mode 100644
index 00000000..81a214cd
--- /dev/null
+++ b/demos/v2/src/example/condition.jsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { render } from '@openinula/next';
+
+function TrafficLight() {
+ let lightIndex = 0;
+ const TRAFFIC_LIGHTS = ['red', 'green'];
+ let light = TRAFFIC_LIGHTS[lightIndex];
+
+ function nextLight() {
+ lightIndex = (lightIndex + 1) % 2;
+ }
+
+ return (
+ <>
+
+
Light is: {light}
+
+ You must
+
+ STOP
+
+
+ GO
+
+
+ >
+ );
+}
+
+render(TrafficLight, document.getElementById('app'));
diff --git a/demos/v2/src/example/formBinding.jsx b/demos/v2/src/example/formBinding.jsx
new file mode 100644
index 00000000..e897c16e
--- /dev/null
+++ b/demos/v2/src/example/formBinding.jsx
@@ -0,0 +1,75 @@
+/*
+ * 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 { render } from '@openinula/next';
+function Form() {
+ let text = 'a';
+ let checked = true;
+ let picked = 'One';
+ let selected = 'A';
+ let multiSelected = [];
+ function updateValue(e) {
+ text = e.target.value;
+ }
+ function handleCheckboxChange(e) {
+ checked = e.target.checked;
+ }
+ function handleRadioChange(e) {
+ picked = e.target.value;
+ }
+ function handleSelectChange(e) {
+ selected = e.target.value;
+ }
+ function handleMultiSelectChange(e) {
+ multiSelected = Array.from(e.target.selectedOptions).map(option => option.value);
+ }
+ return (
+ <>
+
Text Input
+
+
{text}
+
+
Checkbox
+
+
+
+
Radio
+
+
+
+
+
+
Picked: {picked}
+
+
Select
+
+
Selected: {selected}
+
+
Multi Select
+
+
Selected: {multiSelected}
+ >
+ );
+}
+
+render(Form, document.getElementById('app'));
diff --git a/demos/v2/src/example/getData.jsx b/demos/v2/src/example/getData.jsx
new file mode 100644
index 00000000..08a44c29
--- /dev/null
+++ b/demos/v2/src/example/getData.jsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2024 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import { render } from '@openinula/next';
+
+const API_URL = 'https://jsonplaceholder.typicode.com/users';
+const users = await (await fetch(API_URL)).json();
+function List({ arr }) {
+ return (
+
+ {item =>
{item.name}
}
+
+ );
+}
+
+function App() {
+ return ;
+}
+
+render(App, document.getElementById('app'));
diff --git a/demos/v2/src/example/helloWorld.jsx b/demos/v2/src/example/helloWorld.jsx
new file mode 100644
index 00000000..e318d838
--- /dev/null
+++ b/demos/v2/src/example/helloWorld.jsx
@@ -0,0 +1,21 @@
+/*
+ * 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 { render } from '@openinula/next';
+function HelloWorld() {
+ return
Hello World!
;
+}
+
+render(HelloWorld, document.getElementById('app'));
diff --git a/demos/v2/src/example/loop.jsx b/demos/v2/src/example/loop.jsx
new file mode 100644
index 00000000..caefc1d3
--- /dev/null
+++ b/demos/v2/src/example/loop.jsx
@@ -0,0 +1,26 @@
+/*
+ * 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 { render } from '@openinula/next';
+
+function Colors() {
+ const colors = ['red', 'green', 'blue'];
+ return (
+
+ {color =>
{color}
}
+
+ );
+}
+render(Colors, document.getElementById('app'));
diff --git a/demos/v2/src/example/simpleComponent.jsx b/demos/v2/src/example/simpleComponent.jsx
new file mode 100644
index 00000000..2cac4c26
--- /dev/null
+++ b/demos/v2/src/example/simpleComponent.jsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { render } from '@openinula/next';
+
+let fruits = ['apple', 'banana', 'pear'];
+function List({ arr }) {
+ return (
+
+ {item =>
{item}
}
+
+ );
+}
+
+function App() {
+ return ;
+}
+
+render(App, document.getElementById('app'));
diff --git a/demos/v2/src/example/userInput.jsx b/demos/v2/src/example/userInput.jsx
new file mode 100644
index 00000000..873a6201
--- /dev/null
+++ b/demos/v2/src/example/userInput.jsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { render } from '@openinula/next';
+function UserInput() {
+ let count = 0;
+ function incrementCount() {
+ count++;
+ }
+
+ return (
+ <>
+
{count}
+
+ >
+ );
+}
+
+render(UserInput, document.getElementById('app'));
diff --git a/demos/v2/tsconfig.json b/demos/v2/tsconfig.json
new file mode 100644
index 00000000..0f30d84c
--- /dev/null
+++ b/demos/v2/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "jsx": "preserve",
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "Node",
+ "strict": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "noEmit": true,
+ "noUnusedParameters": true,
+ "skipLibCheck": true,
+ "experimentalDecorators": true
+ },
+ "ts-node": {
+ "esm": true
+ }
+}
diff --git a/demos/v2/vite.config.ts b/demos/v2/vite.config.ts
new file mode 100644
index 00000000..005fd20e
--- /dev/null
+++ b/demos/v2/vite.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite';
+import inula from '@openinula/vite-plugin-inula-next';
+
+export default defineConfig({
+ build: {
+ minify: false, // 设置为 false 可以关闭代码压缩
+ },
+ server: {
+ port: 4320,
+ },
+ base: '',
+ optimizeDeps: {
+ disabled: true,
+ },
+ plugins: [inula({ files: '**/*.{ts,js,tsx,jsx}' })],
+});
diff --git a/docs/inula-next/OpenInula 2.0 Parser.md b/docs/inula-next/OpenInula 2.0 Parser.md
new file mode 100644
index 00000000..8465bdf3
--- /dev/null
+++ b/docs/inula-next/OpenInula 2.0 Parser.md
@@ -0,0 +1,27 @@
+# OpenInula 2.0 Parser
+
+This document describes the OpenInula 2.0 parser, which is used to parse the 2.0 API into a HIR (High-level Intermediate
+Representation) that can be used by the OpenInula 2.0 compiler.
+
+## Workflow
+```mermaid
+graph TD
+ A[OpenInula 2.0 Code] --> B[Visitor]
+ B --> C[VariableAnalyze]
+ B --> D[ViewAnalyze]
+ B --> E[fnMacroAnalyze]
+ B --> F[HookAnalyze-TODO]
+ C --> R[ReactivityParser]
+ D --> G[JSXParser]
+ G --> R
+ E --> R
+ F --> R
+ R --> |unused bit pruning|HIR
+```
+
+## Data Structure
+see `types.ts` in `packages/transpiler/babel-inula-next-core/src/analyze/types.ts`
+
+## TODO LIST
+- [x] for analyze the local variable, we need to consider the scope of the variable
+- [ ] hook analyze
diff --git a/docs/inula-next/README.md b/docs/inula-next/README.md
new file mode 100644
index 00000000..39ed8ecb
--- /dev/null
+++ b/docs/inula-next/README.md
@@ -0,0 +1,117 @@
+# Todo-list
+
+- [ ] function 2 class.
+ - [x] assignment 2 property
+ - [x] statement 2 watch func
+ - [ ] handle `props` @HQ
+ - [x] object destructuring
+ - [x] default value
+ - [ ] partial object destructuring
+ - [ ] nested object destructuring
+ - [ ] nested array destructuring
+ - [x] alias
+ - [x] add `this` @HQ
+ - [x] for (jsx-parser) -> playground + benchmark @YH
+ - [x] lifecycle @HQ
+ - [x] ref @HQ (to validate)
+ - [x] env @HQ (to validate)
+ - [ ] Sub component
+ - [ ] Early Return
+ - [ ] custom hook -> Model @YH
+- [ ] JSX
+ - [x] style
+ - [x] fragment
+ - [x] ref (to validate)
+ - [ ] snippet
+ - [x] for
+
+
+# function component syntax
+
+- [ ] props (destructuring | partial destructuring | default value | alias)
+- [ ] variable declaration -> class component property
+- [ ] function declaration ( arrow function | async function )-> class method
+- [ ] Statement -> watch function
+ - [ ] assignment
+ - [ ] function call
+ - [ ] class method call
+ - [ ] for loop
+ - [ ] while loop (do while, while, for, for in, for of)
+ - [ ] if statement
+ - [ ] switch statement
+ - [ ] try catch statement
+ - [ ] throw statement ? not support
+ - [ ] delete expression
+- [ ] lifecycle -> LabeledStatement
+- [ ] return statement -> render method(Body)
+- [ ] iife
+- [ ] early return
+
+
+# Issues
+- [ ] partial props destructuring -> support this.$props @YH
+```jsx
+function Input({onClick, xxx, ...props}) {
+ function handleClick() {
+ onClick()
+ }
+ return
+}
+```
+- [ ] model class declaration should before class component declaration -> use Class polyfill
+```jsx
+// Code like this will cause error: `FetchModel` is not defined
+@Main
+@View
+class MyComp {
+ fetchModel = use(FetchModel, { url: "https://api.example.com/data" })
+
+ Body() {}
+}
+
+@Model
+class FetchModel {}
+```
+- [ ] custom hook early return @YH
+- [ ] snippet
+```jsx
+ const H1 = ;
+ // {H1}
+ const H1 = (name) => ;
+ // {H1()}
+ function H1() {
+ return ;
+ }
+ //
+```
+- [ ] Render text and variable, Got Error
+```jsx
+ // Uncaught DOMException: Failed to execute 'appendChild' on 'Node': This node type does not support this method.
+
+```
+
+# Proposal
+## Watch
+
+自动将Statement包裹Watch的反例:
+```jsx
+ // 前置操作: 场景为Table组件,需要响应column变化,先置空column,再计算新的columnByKey
+ let columnByKey;
+ watch: {
+ columnByKey = {};
+ columns.forEach(col => {
+ columnByKey[col.key] = col;
+ });
+ }
+ // 临时变量: 场景为操作前的计算部分临时变量
+ watch: {
+ let col = columnByKey[sortBy];
+ if (
+ col !== undefined &&
+ col.sortable === true &&
+ typeof col.value === "function"
+ ) {
+ sortFunction = r => col.value(r);
+ }
+ }
+```
diff --git a/package.json b/package.json
index d5f9497e..3d6b47e8 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"build:inula-intl": "pnpm -F inula-intl build",
"build:inula-request": "pnpm -F inula-request build",
"build:inula-router": "pnpm -F inula-router build",
+ "build:transpiler": "pnpm --filter './packages/transpiler/*' run build",
"commitlint": "commitlint --config commitlint.config.js -e",
"postinstall": "husky install"
},
@@ -80,5 +81,9 @@
"engines": {
"node": ">=10.x",
"npm": ">=7.x"
+ },
+ "dependencies": {
+ "@changesets/cli": "^2.27.1",
+ "changeset": "^0.2.6"
}
}
diff --git a/packages/inula-cli/README.md b/packages/inula-cli/README.md
index bebd176e..c119d321 100644
--- a/packages/inula-cli/README.md
+++ b/packages/inula-cli/README.md
@@ -54,7 +54,7 @@ inula-cli的推荐目录结构如下:
│ └── inula-cli
│ ├── lib
├── mock // mock目录
-│ └── mock.ts
+│ └── transform.ts
├── src // 项目源码目录
│ ├── pages
│ │ ├── index.less
@@ -178,10 +178,10 @@ inula-cli的所有功能都围绕插件展开,插件可以很方便地让用
inula-cli支持用户集成已发布在npm仓库的插件,用户可以按需安装并运行这些插件。
-安装可以通过npm安装,这里以插件@inula/add为例:
+安装可以通过npm安装,这里以插件@openinula/add为例:
```shell
-npm i --save-dev @inula/add
+npm i --save-dev @openinula/add
```
如果需要运行插件,需要在配置文件中配置对应的插件路径
@@ -191,7 +191,7 @@ npm i --save-dev @inula/add
export default {
...
- plugins:["@inula/add"]
+ plugins:["@openinula/add"]
}
```
diff --git a/packages/inula-cli/externals.d.ts b/packages/inula-cli/externals.d.ts
index 2fe4981b..ab03cf87 100644
--- a/packages/inula-cli/externals.d.ts
+++ b/packages/inula-cli/externals.d.ts
@@ -14,4 +14,3 @@
*/
declare module 'crequire';
-
diff --git a/packages/inula-cli/src/builtInPlugins/command/dev/buildDev.ts b/packages/inula-cli/src/builtInPlugins/command/dev/buildDev.ts
index 400fbe53..9ffccf76 100644
--- a/packages/inula-cli/src/builtInPlugins/command/dev/buildDev.ts
+++ b/packages/inula-cli/src/builtInPlugins/command/dev/buildDev.ts
@@ -57,7 +57,7 @@ export default (api: API) => {
api.applyHook({ name: 'afterStartDevServer' });
});
} else {
- api.logger.error('Can\'t find config');
+ api.logger.error("Can't find config");
}
break;
case 'vite':
@@ -70,7 +70,7 @@ export default (api: API) => {
server.printUrls();
});
} else {
- api.logger.error('Can\'t find config');
+ api.logger.error("Can't find config");
}
break;
default:
diff --git a/packages/inula-cli/src/builtInPlugins/command/generate/generate.ts b/packages/inula-cli/src/builtInPlugins/command/generate/generate.ts
index 777aefe8..e71f4f96 100644
--- a/packages/inula-cli/src/builtInPlugins/command/generate/generate.ts
+++ b/packages/inula-cli/src/builtInPlugins/command/generate/generate.ts
@@ -33,7 +33,7 @@ export default (api: API) => {
args._.shift();
}
if (args._.length === 0) {
- api.logger.warn('Can\'t find any generate options.');
+ api.logger.warn("Can't find any generate options.");
return;
}
diff --git a/packages/inula-intl/babel.config.js b/packages/inula-intl/babel.config.js
index 7f94d715..90df52a7 100644
--- a/packages/inula-intl/babel.config.js
+++ b/packages/inula-intl/babel.config.js
@@ -16,10 +16,7 @@
const { preset } = require('./jest.config');
module.exports = {
presets: [
- [
- '@babel/preset-env',
- { targets: { node: 'current' } },
- ],
+ ['@babel/preset-env', { targets: { node: 'current' } }],
['@babel/preset-typescript'],
[
'@babel/preset-react',
diff --git a/packages/inula-intl/rollup.config.js b/packages/inula-intl/rollup.config.js
index 86cd6fb2..e0ed9912 100644
--- a/packages/inula-intl/rollup.config.js
+++ b/packages/inula-intl/rollup.config.js
@@ -40,7 +40,7 @@ export default {
{
file: path.resolve(output, 'intl.esm-browser.js'),
format: 'esm',
- }
+ },
],
plugins: [
nodeResolve({
diff --git a/packages/inula-intl/src/parser/mappingRule.ts b/packages/inula-intl/src/parser/mappingRule.ts
index 1dff32ec..54bee482 100644
--- a/packages/inula-intl/src/parser/mappingRule.ts
+++ b/packages/inula-intl/src/parser/mappingRule.ts
@@ -14,11 +14,11 @@
*/
const body: Record = {
- doubleapos: { match: '\'\'', value: () => '\'' },
+ doubleapos: { match: "''", value: () => "'" },
quoted: {
lineBreaks: true,
match: /'[{}#](?:[^]*?[^'])?'(?!')/u,
- value: src => src.slice(1, -1).replace(/''/g, '\''),
+ value: src => src.slice(1, -1).replace(/''/g, "'"),
},
argument: {
lineBreaks: true,
diff --git a/packages/inula-intl/tests/core/I18n.test.ts b/packages/inula-intl/tests/core/I18n.test.ts
index 77079664..e33c7e82 100644
--- a/packages/inula-intl/tests/core/I18n.test.ts
+++ b/packages/inula-intl/tests/core/I18n.test.ts
@@ -90,19 +90,19 @@ describe('I18n', () => {
});
it('._ allow escaping syntax characters', () => {
const messages = {
- 'My \'\'name\'\' is \'{name}\'': 'Mi \'\'nombre\'\' es \'{name}\'',
+ "My ''name'' is '{name}'": "Mi ''nombre'' es '{name}'",
};
const i18n = new I18n({
locale: 'es',
messages: { es: messages },
});
- expect(i18n.formatMessage('My \'\'name\'\' is \'{name}\'')).toEqual('Mi \'nombre\' es {name}');
+ expect(i18n.formatMessage("My ''name'' is '{name}'")).toEqual("Mi 'nombre' es {name}");
});
it('._ should format message from catalog', function () {
const messages = {
Hello: 'Salut',
- id: 'Je m\'appelle {name}',
+ id: "Je m'appelle {name}",
};
const i18n = new I18n({
locale: 'fr',
@@ -110,7 +110,7 @@ describe('I18n', () => {
});
expect(i18n.locale).toEqual('fr');
expect(i18n.formatMessage('Hello')).toEqual('Salut');
- expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual('Je m\'appelle Fred');
+ expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual("Je m'appelle Fred");
});
it('should return the formatted date and time', () => {
diff --git a/packages/inula-intl/tests/utils/eventListener.test.ts b/packages/inula-intl/tests/utils/eventListener.test.ts
index 17e0f43b..c72af6cf 100644
--- a/packages/inula-intl/tests/utils/eventListener.test.ts
+++ b/packages/inula-intl/tests/utils/eventListener.test.ts
@@ -43,7 +43,7 @@ describe('eventEmitter', () => {
expect(listener).not.toBeCalled();
});
- it('should do nothing when even doesn\'t exist', () => {
+ it("should do nothing when even doesn't exist", () => {
const unknown = jest.fn();
const emitter = new EventEmitter();
diff --git a/packages/inula-next-shared/package.json b/packages/inula-next-shared/package.json
new file mode 100644
index 00000000..a81c26c9
--- /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 00000000..798bdbbc
--- /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 00000000..c9c265f5
--- /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-store/package.json b/packages/inula-next-store/package.json
new file mode 100644
index 00000000..1736509a
--- /dev/null
+++ b/packages/inula-next-store/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@openinula/store",
+ "version": "0.0.1",
+ "description": "Inula shared store",
+ "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": {
+ "tsup": "^6.5.0",
+ "typescript": "^5.3.2"
+ },
+ "tsup": {
+ "entry": [
+ "src/index.ts"
+ ],
+ "format": [
+ "esm"
+ ],
+ "clean": true,
+ "dts": true,
+ "minify": true
+ }
+}
diff --git a/packages/inula-next-store/src/index.ts b/packages/inula-next-store/src/index.ts
new file mode 100644
index 00000000..7dc7fbfd
--- /dev/null
+++ b/packages/inula-next-store/src/index.ts
@@ -0,0 +1 @@
+export const Store = {};
diff --git a/packages/inula-next-store/tsconfig.json b/packages/inula-next-store/tsconfig.json
new file mode 100644
index 00000000..ad175791
--- /dev/null
+++ b/packages/inula-next-store/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
new file mode 100644
index 00000000..b23a459b
--- /dev/null
+++ b/packages/inula-next/README.md
@@ -0,0 +1,2 @@
+# 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
new file mode 100644
index 00000000..b0b17f3c
--- /dev/null
+++ b/packages/inula-next/package.json
@@ -0,0 +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",
+ "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 00000000..ad04d88d
--- /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.ts b/packages/inula-next/src/CompNode.ts
new file mode 100644
index 00000000..c2636ee9
--- /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 00000000..ef10dedf
--- /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/HookNode.ts b/packages/inula-next/src/HookNode.ts
new file mode 100644
index 00000000..c6206a29
--- /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 00000000..01b026e3
--- /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.ts b/packages/inula-next/src/MutableNode/CondNode.ts
new file mode 100644
index 00000000..932bdf67
--- /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.ts b/packages/inula-next/src/MutableNode/ExpNode.ts
new file mode 100644
index 00000000..6238d23e
--- /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/ForNode.ts b/packages/inula-next/src/MutableNode/ForNode.ts
new file mode 100644
index 00000000..301173c5
--- /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/mutableHandler.ts b/packages/inula-next/src/MutableNode/mutableHandler.ts
new file mode 100644
index 00000000..b2e00b65
--- /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/equal.ts b/packages/inula-next/src/equal.ts
new file mode 100644
index 00000000..d5a27fe1
--- /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.ts b/packages/inula-next/src/index.ts
new file mode 100644
index 00000000..87ba1887
--- /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 00000000..5be0ef89
--- /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/renderer/dom.ts b/packages/inula-next/src/renderer/dom.ts
new file mode 100644
index 00000000..39803d6f
--- /dev/null
+++ b/packages/inula-next/src/renderer/dom.ts
@@ -0,0 +1,222 @@
+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.ts b/packages/inula-next/src/scheduler.ts
new file mode 100644
index 00000000..73d900ca
--- /dev/null
+++ b/packages/inula-next/src/scheduler.ts
@@ -0,0 +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.
+ */
+export function schedule(task: () => void) {
+ p.then(task);
+}
diff --git a/packages/inula-next/src/types.ts b/packages/inula-next/src/types.ts
new file mode 100644
index 00000000..da44a766
--- /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