From 83c80341dcc21411a9033a366d158b14324c801a Mon Sep 17 00:00:00 2001
From: Hoikan <408255371@qq.com>
Date: Wed, 3 Apr 2024 08:41:11 +0000
Subject: [PATCH] !168 inula api2.0 * feat: inula-next init * feat: v2 init *
feat(class-transform): add watch decorator * feat(class-transform): update
docs * feat(class-transform): init
---
.gitignore | 4 +-
demos/v2/index.html | 12 +
demos/v2/package.json | 25 +
demos/v2/src/App.css | 3 +
demos/v2/src/App.view.tsx | 77 ++
demos/v2/tsconfig.json | 20 +
demos/v2/vite.config.ts | 13 +
package.json | 1 +
packages/inula-cli/README.md | 2 +-
packages/inula-cli/externals.d.ts | 1 -
.../builtInPlugins/command/dev/buildDev.ts | 4 +-
.../command/generate/generate.ts | 2 +-
packages/inula-intl/babel.config.js | 5 +-
packages/inula-intl/rollup.config.js | 2 +-
packages/inula-intl/src/parser/mappingRule.ts | 4 +-
packages/inula-intl/tests/core/I18n.test.ts | 8 +-
.../tests/utils/eventListener.test.ts | 2 +-
packages/inula-request/babel.config.js | 8 +-
packages/inula-request/rollup.config.js | 23 +-
.../router/matcher/__tests__/lexer.test.ts | 2 +-
packages/inula-v2-store/package.json | 38 +
packages/inula-v2-store/src/index.ts | 1 +
packages/inula-v2-store/tsconfig.json | 14 +
packages/inula-v2/README.md | 2 +
packages/inula-v2/package.json | 41 +
packages/inula-v2/src/CompNode.js | 367 ++++++
packages/inula-v2/src/DLNode.js | 209 ++++
packages/inula-v2/src/EnvNode.js | 103 ++
packages/inula-v2/src/HTMLNode.js | 163 +++
packages/inula-v2/src/MutableNode/CondNode.js | 69 ++
packages/inula-v2/src/MutableNode/ExpNode.js | 86 ++
packages/inula-v2/src/MutableNode/FlatNode.js | 33 +
packages/inula-v2/src/MutableNode/ForNode.js | 406 +++++++
.../inula-v2/src/MutableNode/MutableNode.js | 71 ++
packages/inula-v2/src/MutableNode/TryNode.js | 45 +
packages/inula-v2/src/PropView.js | 48 +
packages/inula-v2/src/SnippetNode.js | 18 +
packages/inula-v2/src/TextNode.js | 24 +
packages/inula-v2/src/index.d.ts | 1 +
packages/inula-v2/src/index.js | 56 +
packages/inula-v2/src/store.js | 42 +
packages/inula-v2/src/types/compTag.d.ts | 74 ++
packages/inula-v2/src/types/envTag.d.ts | 6 +
.../inula-v2/src/types/expressionTag.d.ts | 13 +
.../inula-v2/src/types/htmlTag/event.d.ts | 516 +++++++++
.../src/types/htmlTag/htmlElement.d.ts | 33 +
.../inula-v2/src/types/htmlTag/index.d.ts | 236 ++++
packages/inula-v2/src/types/index.d.ts | 41 +
packages/inula-v2/src/types/model.d.ts | 22 +
packages/inula-v2/tsconfig.json | 13 +
.../inula/scripts/rollup/rollup.config.js | 17 +-
packages/inula/src/inulax/types.ts | 12 +-
.../transpiler/class-transformer/package.json | 43 +
.../transpiler/class-transformer/src/index.ts | 31 +
.../class-transformer/src/plugin.ts | 32 +
.../class-transformer/src/pluginProvider.ts | 318 +++++
.../src/test/component-composition.test.ts | 153 +++
.../src/test/conditional.test.ts | 19 +
.../src/test/fn2Class.test.ts | 299 +++++
.../class-transformer/src/test/transform.ts | 23 +
.../transpiler/class-transformer/src/types.ts | 7 +
.../transpiler/error-handler/package.json | 33 +
.../transpiler/error-handler/src/index.ts | 62 +
.../transpiler/error-handler/tsconfig.json | 12 +
packages/transpiler/jsx-parser/package.json | 43 +
packages/transpiler/jsx-parser/src/index.ts | 14 +
packages/transpiler/jsx-parser/src/parser.ts | 541 +++++++++
.../jsx-parser/src/test/ElementUnit.test.ts | 198 ++++
.../jsx-parser/src/test/ExpUnit.test.ts | 87 ++
.../jsx-parser/src/test/IfUnit.test.ts | 201 ++++
.../jsx-parser/src/test/TemplateUnit.test.ts | 82 ++
.../jsx-parser/src/test/TextUnit.test.ts | 73 ++
.../jsx-parser/src/test/global.d.ts | 1 +
.../transpiler/jsx-parser/src/test/mock.ts | 228 ++++
packages/transpiler/jsx-parser/src/types.ts | 85 ++
packages/transpiler/jsx-parser/tsconfig.json | 13 +
.../transpiler/reactivity-parser/package.json | 47 +
.../transpiler/reactivity-parser/src/error.ts | 5 +
.../transpiler/reactivity-parser/src/index.ts | 25 +
.../reactivity-parser/src/parser.ts | 1023 +++++++++++++++++
.../src/test/Dependency.test.ts | 77 ++
.../src/test/MutableTagParticle.test.ts | 68 ++
.../src/test/OtherParticle.test.ts | 73 ++
.../src/test/Template.test.ts | 68 ++
.../reactivity-parser/src/test/mock.ts | 250 ++++
.../transpiler/reactivity-parser/src/types.ts | 135 +++
.../reactivity-parser/tsconfig.json | 13 +
.../transpiler/view-generator/package.json | 44 +
.../src/HelperGenerators/BaseGenerator.ts | 284 +++++
.../src/HelperGenerators/CondGenerator.ts | 112 ++
.../src/HelperGenerators/ElementGenerator.ts | 52 +
.../HelperGenerators/ForwardPropGenerator.ts | 16 +
.../src/HelperGenerators/HTMLPropGenerator.ts | 363 ++++++
.../HelperGenerators/LifecycleGenerator.ts | 66 ++
.../src/HelperGenerators/PropViewGenerator.ts | 123 ++
.../view-generator/src/MainViewGenerator.ts | 82 ++
.../src/NodeGenerators/CompGenerator.ts | 153 +++
.../src/NodeGenerators/EnvGenerator.ts | 95 ++
.../src/NodeGenerators/ExpGenerator.ts | 76 ++
.../src/NodeGenerators/ForGenerator.ts | 131 +++
.../src/NodeGenerators/HTMLGenerator.ts | 88 ++
.../src/NodeGenerators/IfGenerator.ts | 96 ++
.../src/NodeGenerators/SnippetGenerator.ts | 136 +++
.../src/NodeGenerators/SwitchGenerator.ts | 106 ++
.../src/NodeGenerators/TemplateGenerator.ts | 282 +++++
.../src/NodeGenerators/TextGenerator.ts | 54 +
.../src/NodeGenerators/TryGenerator.ts | 97 ++
.../view-generator/src/SnippetGenerator.ts | 184 +++
.../view-generator/src/ViewGenerator.ts | 118 ++
.../transpiler/view-generator/src/error.ts | 14 +
.../transpiler/view-generator/src/index.ts | 23 +
.../transpiler/view-generator/src/types.ts | 12 +
.../transpiler/view-generator/tsconfig.json | 13 +
packages/transpiler/view-parser/package.json | 46 +
packages/transpiler/view-parser/src/error.ts | 22 +
packages/transpiler/view-parser/src/index.ts | 16 +
packages/transpiler/view-parser/src/parser.ts | 597 ++++++++++
.../view-parser/src/test/CompUnit.test.ts | 160 +++
.../view-parser/src/test/EnvUnit.test.ts | 92 ++
.../view-parser/src/test/ExpUnit.test.ts | 119 ++
.../view-parser/src/test/ForUnit.test.ts | 0
.../view-parser/src/test/HTMLUnit.test.ts | 156 +++
.../view-parser/src/test/IfUnit.test.ts | 72 ++
.../view-parser/src/test/SubviewUnit.test.ts | 97 ++
.../view-parser/src/test/SwitchUnit.test.ts | 60 +
.../view-parser/src/test/TextUnit.test.ts | 86 ++
.../view-parser/src/test/TryUnit.test.ts | 0
.../transpiler/view-parser/src/test/mock.ts | 224 ++++
packages/transpiler/view-parser/src/types.ts | 100 ++
packages/transpiler/view-parser/tsconfig.json | 13 +
.../vite-plugin-inula-next/dist/index.js | 2 +
.../vite-plugin-inula-next/dist/index.js.map | 1 +
.../vite-plugin-inula-next/package.json | 45 +
.../vite-plugin-inula-next/src/index.ts | 40 +
.../vite-plugin-inula-next/tsconfig.json | 13 +
pnpm-workspace.yaml | 2 +
136 files changed, 12157 insertions(+), 46 deletions(-)
create mode 100644 demos/v2/index.html
create mode 100644 demos/v2/package.json
create mode 100644 demos/v2/src/App.css
create mode 100644 demos/v2/src/App.view.tsx
create mode 100644 demos/v2/tsconfig.json
create mode 100644 demos/v2/vite.config.ts
create mode 100644 packages/inula-v2-store/package.json
create mode 100644 packages/inula-v2-store/src/index.ts
create mode 100644 packages/inula-v2-store/tsconfig.json
create mode 100644 packages/inula-v2/README.md
create mode 100644 packages/inula-v2/package.json
create mode 100644 packages/inula-v2/src/CompNode.js
create mode 100644 packages/inula-v2/src/DLNode.js
create mode 100644 packages/inula-v2/src/EnvNode.js
create mode 100644 packages/inula-v2/src/HTMLNode.js
create mode 100644 packages/inula-v2/src/MutableNode/CondNode.js
create mode 100644 packages/inula-v2/src/MutableNode/ExpNode.js
create mode 100644 packages/inula-v2/src/MutableNode/FlatNode.js
create mode 100644 packages/inula-v2/src/MutableNode/ForNode.js
create mode 100644 packages/inula-v2/src/MutableNode/MutableNode.js
create mode 100644 packages/inula-v2/src/MutableNode/TryNode.js
create mode 100644 packages/inula-v2/src/PropView.js
create mode 100644 packages/inula-v2/src/SnippetNode.js
create mode 100644 packages/inula-v2/src/TextNode.js
create mode 100644 packages/inula-v2/src/index.d.ts
create mode 100644 packages/inula-v2/src/index.js
create mode 100644 packages/inula-v2/src/store.js
create mode 100644 packages/inula-v2/src/types/compTag.d.ts
create mode 100644 packages/inula-v2/src/types/envTag.d.ts
create mode 100644 packages/inula-v2/src/types/expressionTag.d.ts
create mode 100644 packages/inula-v2/src/types/htmlTag/event.d.ts
create mode 100644 packages/inula-v2/src/types/htmlTag/htmlElement.d.ts
create mode 100644 packages/inula-v2/src/types/htmlTag/index.d.ts
create mode 100644 packages/inula-v2/src/types/index.d.ts
create mode 100644 packages/inula-v2/src/types/model.d.ts
create mode 100644 packages/inula-v2/tsconfig.json
create mode 100644 packages/transpiler/class-transformer/package.json
create mode 100644 packages/transpiler/class-transformer/src/index.ts
create mode 100644 packages/transpiler/class-transformer/src/plugin.ts
create mode 100644 packages/transpiler/class-transformer/src/pluginProvider.ts
create mode 100644 packages/transpiler/class-transformer/src/test/component-composition.test.ts
create mode 100644 packages/transpiler/class-transformer/src/test/conditional.test.ts
create mode 100644 packages/transpiler/class-transformer/src/test/fn2Class.test.ts
create mode 100644 packages/transpiler/class-transformer/src/test/transform.ts
create mode 100644 packages/transpiler/class-transformer/src/types.ts
create mode 100644 packages/transpiler/error-handler/package.json
create mode 100644 packages/transpiler/error-handler/src/index.ts
create mode 100644 packages/transpiler/error-handler/tsconfig.json
create mode 100644 packages/transpiler/jsx-parser/package.json
create mode 100644 packages/transpiler/jsx-parser/src/index.ts
create mode 100644 packages/transpiler/jsx-parser/src/parser.ts
create mode 100644 packages/transpiler/jsx-parser/src/test/ElementUnit.test.ts
create mode 100644 packages/transpiler/jsx-parser/src/test/ExpUnit.test.ts
create mode 100644 packages/transpiler/jsx-parser/src/test/IfUnit.test.ts
create mode 100644 packages/transpiler/jsx-parser/src/test/TemplateUnit.test.ts
create mode 100644 packages/transpiler/jsx-parser/src/test/TextUnit.test.ts
create mode 100644 packages/transpiler/jsx-parser/src/test/global.d.ts
create mode 100644 packages/transpiler/jsx-parser/src/test/mock.ts
create mode 100644 packages/transpiler/jsx-parser/src/types.ts
create mode 100644 packages/transpiler/jsx-parser/tsconfig.json
create mode 100644 packages/transpiler/reactivity-parser/package.json
create mode 100644 packages/transpiler/reactivity-parser/src/error.ts
create mode 100644 packages/transpiler/reactivity-parser/src/index.ts
create mode 100644 packages/transpiler/reactivity-parser/src/parser.ts
create mode 100644 packages/transpiler/reactivity-parser/src/test/Dependency.test.ts
create mode 100644 packages/transpiler/reactivity-parser/src/test/MutableTagParticle.test.ts
create mode 100644 packages/transpiler/reactivity-parser/src/test/OtherParticle.test.ts
create mode 100644 packages/transpiler/reactivity-parser/src/test/Template.test.ts
create mode 100644 packages/transpiler/reactivity-parser/src/test/mock.ts
create mode 100644 packages/transpiler/reactivity-parser/src/types.ts
create mode 100644 packages/transpiler/reactivity-parser/tsconfig.json
create mode 100644 packages/transpiler/view-generator/package.json
create mode 100644 packages/transpiler/view-generator/src/HelperGenerators/BaseGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/HelperGenerators/CondGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/HelperGenerators/ElementGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/HelperGenerators/ForwardPropGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/HelperGenerators/HTMLPropGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/HelperGenerators/LifecycleGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/HelperGenerators/PropViewGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/MainViewGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/NodeGenerators/CompGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/NodeGenerators/EnvGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/NodeGenerators/ExpGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/NodeGenerators/ForGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/NodeGenerators/HTMLGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/NodeGenerators/IfGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/NodeGenerators/SnippetGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/NodeGenerators/SwitchGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/NodeGenerators/TemplateGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/NodeGenerators/TextGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/NodeGenerators/TryGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/SnippetGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/ViewGenerator.ts
create mode 100644 packages/transpiler/view-generator/src/error.ts
create mode 100644 packages/transpiler/view-generator/src/index.ts
create mode 100644 packages/transpiler/view-generator/src/types.ts
create mode 100644 packages/transpiler/view-generator/tsconfig.json
create mode 100644 packages/transpiler/view-parser/package.json
create mode 100644 packages/transpiler/view-parser/src/error.ts
create mode 100644 packages/transpiler/view-parser/src/index.ts
create mode 100644 packages/transpiler/view-parser/src/parser.ts
create mode 100644 packages/transpiler/view-parser/src/test/CompUnit.test.ts
create mode 100644 packages/transpiler/view-parser/src/test/EnvUnit.test.ts
create mode 100644 packages/transpiler/view-parser/src/test/ExpUnit.test.ts
create mode 100644 packages/transpiler/view-parser/src/test/ForUnit.test.ts
create mode 100644 packages/transpiler/view-parser/src/test/HTMLUnit.test.ts
create mode 100644 packages/transpiler/view-parser/src/test/IfUnit.test.ts
create mode 100644 packages/transpiler/view-parser/src/test/SubviewUnit.test.ts
create mode 100644 packages/transpiler/view-parser/src/test/SwitchUnit.test.ts
create mode 100644 packages/transpiler/view-parser/src/test/TextUnit.test.ts
create mode 100644 packages/transpiler/view-parser/src/test/TryUnit.test.ts
create mode 100644 packages/transpiler/view-parser/src/test/mock.ts
create mode 100644 packages/transpiler/view-parser/src/types.ts
create mode 100644 packages/transpiler/view-parser/tsconfig.json
create mode 100644 packages/transpiler/vite-plugin-inula-next/dist/index.js
create mode 100644 packages/transpiler/vite-plugin-inula-next/dist/index.js.map
create mode 100644 packages/transpiler/vite-plugin-inula-next/package.json
create mode 100644 packages/transpiler/vite-plugin-inula-next/src/index.ts
create mode 100644 packages/transpiler/vite-plugin-inula-next/tsconfig.json
diff --git a/.gitignore b/.gitignore
index 8636e699..d8b9e23e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,10 @@
-/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
diff --git a/demos/v2/index.html b/demos/v2/index.html
new file mode 100644
index 00000000..28fb9622
--- /dev/null
+++ b/demos/v2/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Dlight.JS
+
+
+
+
+
+
+
diff --git a/demos/v2/package.json b/demos/v2/package.json
new file mode 100644
index 00000000..63425f3a
--- /dev/null
+++ b/demos/v2/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "dev",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@babel/standalone": "^7.22.4",
+ "@inula/next": "workspace:*",
+ "@iandx/easy-css": "^0.10.14",
+ "babel-preset-inula-next": "workspace:*"
+ },
+ "devDependencies": {
+ "typescript": "^5.2.2",
+ "vite": "^4.4.9",
+ "vite-plugin-inula-next": "workspace:*"
+ },
+ "keywords": [
+ "dlight.js"
+ ]
+}
diff --git a/demos/v2/src/App.css b/demos/v2/src/App.css
new file mode 100644
index 00000000..73c673f8
--- /dev/null
+++ b/demos/v2/src/App.css
@@ -0,0 +1,3 @@
+.ok {
+ color: var(--color-ok);
+}
\ No newline at end of file
diff --git a/demos/v2/src/App.view.tsx b/demos/v2/src/App.view.tsx
new file mode 100644
index 00000000..0e47914b
--- /dev/null
+++ b/demos/v2/src/App.view.tsx
@@ -0,0 +1,77 @@
+// @ts-nocheck
+import {
+ Children,
+ Content,
+ Main,
+ Model,
+ Prop,
+ View,
+ Watch,
+ button,
+ div,
+ input,
+ insertChildren,
+ use,
+ render,
+} from '@inula/next';
+
+// @ts-ignore
+function Button({ children, onClick }) {
+ return (
+
+ );
+}
+
+function ArrayModification() {
+ let arr = [];
+ willMount(() => {});
+ return (
+
+ ArrayModification
+ {arr.join(',')}
+
+
+ );
+}
+
+function MyComp() {
+ let count = 0;
+ let db = count * 2;
+ return (
+ <>
+ Hello dlight fn comp
+
+ count: {count}, double is: {db}
+
+
+
+
+
+ >
+ );
+}
+
+function ConditionalRendering({ count }) {
+ return (
+
+ Condition
+ 1}>{count} is bigger than is 1
+ {count} is equal to 1
+ {count} is smaller than 1
+
+ );
+}
+
+render('main', MyComp);
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..7029d548
--- /dev/null
+++ b/demos/v2/vite.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite';
+import inula from 'vite-plugin-inula-next';
+
+export default defineConfig({
+ server: {
+ port: 4320,
+ },
+ base: '',
+ optimizeDeps: {
+ disabled: true,
+ },
+ plugins: [inula({ files: '**/*.{view,model}.{ts,js,tsx,jsx}', enableDevTools: true })],
+});
diff --git a/package.json b/package.json
index d5f9497e..4eaba18d 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"
},
diff --git a/packages/inula-cli/README.md b/packages/inula-cli/README.md
index bebd176e..e60a1cd2 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
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-request/babel.config.js b/packages/inula-request/babel.config.js
index 383563ed..f373490c 100644
--- a/packages/inula-request/babel.config.js
+++ b/packages/inula-request/babel.config.js
@@ -14,11 +14,5 @@
*/
module.exports = {
- presets: [
- [
- '@babel/preset-env',
- { targets: { node: 'current' }},
- ],
- ['@babel/preset-typescript'],
- ],
+ presets: [['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-typescript']],
};
diff --git a/packages/inula-request/rollup.config.js b/packages/inula-request/rollup.config.js
index e7e32556..b1ad23b7 100644
--- a/packages/inula-request/rollup.config.js
+++ b/packages/inula-request/rollup.config.js
@@ -21,16 +21,19 @@ import { babel } from '@rollup/plugin-babel';
export default {
input: './index.ts',
- output: [{
- file: 'dist/inulaRequest.js',
- format: 'umd',
- exports: 'named',
- name: 'inulaRequest',
- sourcemap: false,
- }, {
- file: 'dist/inulaRequest.esm-browser.js',
- format: 'esm',
- }],
+ output: [
+ {
+ file: 'dist/inulaRequest.js',
+ format: 'umd',
+ exports: 'named',
+ name: 'inulaRequest',
+ sourcemap: false,
+ },
+ {
+ file: 'dist/inulaRequest.esm-browser.js',
+ format: 'esm',
+ },
+ ],
plugins: [
resolve(),
commonjs(),
diff --git a/packages/inula-router/src/router/matcher/__tests__/lexer.test.ts b/packages/inula-router/src/router/matcher/__tests__/lexer.test.ts
index f6741216..f5a3725e 100644
--- a/packages/inula-router/src/router/matcher/__tests__/lexer.test.ts
+++ b/packages/inula-router/src/router/matcher/__tests__/lexer.test.ts
@@ -38,7 +38,7 @@ describe('path lexer Test', () => {
expect(tokens).toStrictEqual([{ type: 'delimiter', value: '/' }]);
});
- it('don\'t start with a slash', () => {
+ it("don't start with a slash", () => {
const func = () => lexer('abc.com');
expect(func).toThrow(Error('Url must start with "/".'));
});
diff --git a/packages/inula-v2-store/package.json b/packages/inula-v2-store/package.json
new file mode 100644
index 00000000..cf40cc8e
--- /dev/null
+++ b/packages/inula-v2-store/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@inula/store",
+ "version": "0.0.0",
+ "description": "DLight shared store",
+ "author": {
+ "name": "IanDx",
+ "email": "iandxssxx@gmail.com"
+ },
+ "keywords": [
+ "dlight.js"
+ ],
+ "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-v2-store/src/index.ts b/packages/inula-v2-store/src/index.ts
new file mode 100644
index 00000000..7dc7fbfd
--- /dev/null
+++ b/packages/inula-v2-store/src/index.ts
@@ -0,0 +1 @@
+export const Store = {};
diff --git a/packages/inula-v2-store/tsconfig.json b/packages/inula-v2-store/tsconfig.json
new file mode 100644
index 00000000..ad175791
--- /dev/null
+++ b/packages/inula-v2-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-v2/README.md b/packages/inula-v2/README.md
new file mode 100644
index 00000000..6f89397b
--- /dev/null
+++ b/packages/inula-v2/README.md
@@ -0,0 +1,2 @@
+# DLight Main Package
+See the website's documentations for usage.
diff --git a/packages/inula-v2/package.json b/packages/inula-v2/package.json
new file mode 100644
index 00000000..9136e106
--- /dev/null
+++ b/packages/inula-v2/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@inula/next",
+ "version": "1.0.0-next.9",
+ "author": {
+ "name": "IanDx",
+ "email": "iandxssxx@gmail.com"
+ },
+ "keywords": [
+ "dlight.js"
+ ],
+ "license": "MIT",
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "type": "module",
+ "main": "dist/index.cjs",
+ "module": "dist/index.js",
+ "typings": "dist/index.d.ts",
+ "scripts": {
+ "build": "tsup --sourcemap && cp src/index.d.ts dist/ && cp -r src/types dist/"
+ },
+ "dependencies": {
+ "csstype": "^3.1.3",
+ "@inula/store": "workspace:*"
+ },
+ "devDependencies": {
+ "tsup": "^6.5.0"
+ },
+ "tsup": {
+ "entry": [
+ "src/index.js"
+ ],
+ "format": [
+ "cjs",
+ "esm"
+ ],
+ "clean": true,
+ "minify": true
+ }
+}
diff --git a/packages/inula-v2/src/CompNode.js b/packages/inula-v2/src/CompNode.js
new file mode 100644
index 00000000..a56e56bc
--- /dev/null
+++ b/packages/inula-v2/src/CompNode.js
@@ -0,0 +1,367 @@
+import { DLNode, DLNodeType } from './DLNode';
+import { forwardHTMLProp } from './HTMLNode';
+import { DLStore, cached } from './store';
+
+export class CompNode extends DLNode {
+ /**
+ * @brief Constructor, Comp type
+ * @internal
+ * * key - private property key
+ * * $$key - dependency number, e.g. 0b1, 0b10, 0b100
+ * * $s$key - set of properties that depend on this property
+ * * $p$key - exist if this property is a prop
+ * * $e$key - exist if this property is an env
+ * * $en$key - exist if this property is an env, and it's the innermost env that contains this env
+ * * $w$key - exist if this property is a watcher
+ * * $f$key - a function that returns the value of this property, called when the property's dependencies change
+ * * _$children - children nodes of type PropView
+ * * _$contentKey - the key key of the content prop
+ * * _$forwardProps - exist if this node is forwarding props
+ * * _$forwardPropsId - the keys of the props that this node is forwarding, collected in _$setForwardProp
+ * * _$forwardPropsSet - contain all the nodes that are forwarding props to this node, collected with _$addForwardProps
+ */
+ constructor() {
+ super(DLNodeType.Comp);
+ }
+
+ /**
+ * @brief Init function, called explicitly in the subclass's constructor
+ * @param props - Object containing properties
+ * @param content - Content to be used
+ * @param children - Child nodes
+ * @param forwardPropsScope - Scope for forwarding properties
+ */
+ _$init(props, content, children, forwardPropsScope) {
+ this._$notInitd = true;
+
+ // ---- Forward props first to allow internal props to override forwarded props
+ if (forwardPropsScope) forwardPropsScope._$addForwardProps(this);
+ if (content) this._$setContent(() => content[0], content[1]);
+ if (props)
+ props.forEach(([key, value, deps]) => {
+ if (key === 'props') return this._$setProps(() => value, deps);
+ this._$setProp(key, () => value, deps);
+ });
+ if (children) this._$children = children;
+
+ // ---- Add envs
+ DLStore.global.DLEnvStore &&
+ Object.entries(DLStore.global.DLEnvStore.envs).forEach(([key, [value, envNode]]) => {
+ if (key === '_$catchable') {
+ this._$catchable = value;
+ return;
+ }
+ if (!(`$e$${key}` in this)) return;
+ envNode.addNode(this);
+ this._$initEnv(key, value, envNode);
+ });
+
+ const willCall = () => {
+ this._$callUpdatesBeforeInit();
+ this.didMount && DLNode.addDidMount(this, this.didMount.bind(this));
+ this.willUnmount && DLNode.addWillUnmount(this, this.willUnmount.bind(this));
+ DLNode.addDidUnmount(this, this._$setUnmounted.bind(this));
+ this.didUnmount && DLNode.addDidUnmount(this, this.didUnmount.bind(this));
+ this.willMount?.();
+ this._$nodes = this.Body?.() ?? [];
+ };
+
+ if (this._$catchable) {
+ this._$catchable(willCall)();
+ if (this._$update) this._$update = this._$catchable(this._$update.bind(this));
+ this._$updateDerived = this._$catchable(this._$updateDerived.bind(this));
+ delete this._$catchable;
+ } else {
+ willCall();
+ }
+ }
+
+ _$setUnmounted() {
+ this._$unmounted = true;
+ }
+
+ /**
+ * @brief Call updates manually before the node is mounted
+ */
+ _$callUpdatesBeforeInit() {
+ const protoProps = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
+ const ownProps = Object.getOwnPropertyNames(this);
+ const allProps = [...protoProps, ...ownProps];
+ allProps.forEach(key => {
+ // ---- Run watcher
+ if (key.startsWith('$w$')) return this[key.slice(3)]();
+ // ---- Run model update
+ if (key.startsWith('$md$')) {
+ const realKey = key.slice(4);
+ this[realKey] = this[realKey]();
+ return;
+ }
+ // ---- Run derived value
+ if (key.startsWith('$f$')) {
+ const realKey = key.slice(3);
+ this[realKey] = this[key];
+ this._$updateDerived(realKey);
+ }
+ });
+ delete this._$notInitd;
+ }
+
+ /**
+ * @brief Set all the props to forward
+ * @param key
+ * @param value
+ * @param deps
+ */
+ _$setPropToForward(key, value, deps) {
+ this._$forwardPropsSet.forEach(node => {
+ const isContent = key === '_$content';
+ if (node._$dlNodeType === DLNodeType.Comp) {
+ if (isContent) node._$setContent(() => value, deps);
+ else node._$setProp(key, () => value, deps);
+ return;
+ }
+ if (node instanceof HTMLElement) {
+ if (isContent) key = 'textContent';
+ forwardHTMLProp(node, key, () => value, deps);
+ }
+ });
+ }
+
+ /**
+ * @brief Define forward props
+ * @param key
+ * @param value
+ */
+ _$setForwardProp(key, valueFunc, deps) {
+ const notInitd = '_$notInitd' in this;
+ if (!notInitd && this._$cache(key, deps)) return;
+ const value = valueFunc();
+ if (key === '_$content' && this._$contentKey) {
+ this[this._$contentKey] = value;
+ this._$updateDerived(this._$contentKey);
+ }
+ this[key] = value;
+ this._$updateDerived(key);
+ if (notInitd) this._$forwardPropsId.push(key);
+ else this._$setPropToForward(key, value, deps);
+ }
+
+ /**
+ * @brief Add a node to the set of nodes that are forwarding props to this node and init these props
+ * @param node
+ */
+ _$addForwardProps(node) {
+ this._$forwardPropsSet.add(node);
+ this._$forwardPropsId.forEach(key => {
+ this._$setPropToForward(key, this[key], []);
+ });
+ DLNode.addWillUnmount(node, this._$forwardPropsSet.delete.bind(this._$forwardPropsSet, node));
+ }
+
+ /**
+ * @brief Cache the deps and return true if the deps are the same as the previous deps
+ * @param key
+ * @param deps
+ * @returns
+ */
+ _$cache(key, deps) {
+ if (!deps || !deps.length) return false;
+ const cacheKey = `$cc$${key}`;
+ if (cached(deps, this[cacheKey])) return true;
+ this[cacheKey] = deps;
+ return false;
+ }
+
+ /**
+ * @brief Set the content prop, the key is stored in _$contentKey
+ * @param value
+ */
+ _$setContent(valueFunc, deps) {
+ if ('_$forwardProps' in this) return this._$setForwardProp('_$content', valueFunc, deps);
+ const contentKey = this._$contentKey;
+ if (!contentKey) return;
+ if (this._$cache(contentKey, deps)) return;
+ this[contentKey] = valueFunc();
+ this._$updateDerived(contentKey);
+ }
+
+ /**
+ * @brief Set a prop directly, if this is a forwarded prop, go and init forwarded props
+ * @param key
+ * @param value
+ * @param deps
+ */
+ _$setProp(key, valueFunc, deps) {
+ if ('_$forwardProps' in this) return this._$setForwardProp(key, valueFunc, deps);
+ if (!(`$p$${key}` in this)) {
+ console.warn(`[${key}] is not a prop in ${this.constructor.name}`);
+ return;
+ }
+ if (this._$cache(key, deps)) return;
+ this[key] = valueFunc();
+ this._$updateDerived(key);
+ }
+
+ _$setProps(valueFunc, deps) {
+ if (this._$cache('props', deps)) return;
+ const props = valueFunc();
+ if (!props) return;
+ Object.entries(props).forEach(([key, value]) => {
+ this._$setProp(key, () => value, []);
+ });
+ }
+
+ /**
+ * @brief Init an env, put the corresponding innermost envNode in $en$key
+ * @param key
+ * @param value
+ * @param envNode
+ */
+ _$initEnv(key, value, envNode) {
+ this[key] = value;
+ this[`$en$${key}`] = envNode;
+ }
+
+ // ---- Update functions
+ /**
+ * @brief Update an env, called in EnvNode._$update
+ * @param key
+ * @param value
+ * @param envNode
+ */
+ _$updateEnv(key, value, envNode) {
+ if (!(`$e$${key}` in this)) return;
+ if (envNode !== this[`$en$${key}`]) return;
+ this[key] = value;
+ this._$updateDerived(key);
+ }
+
+ /**
+ * @brief Update a prop
+ */
+ _$ud(exp, key) {
+ this._$updateDerived(key);
+ return exp;
+ }
+
+ /**
+ * @brief Update properties that depend on this property
+ * @param key
+ */
+ _$updateDerived(key) {
+ if ('_$notInitd' in this) return;
+
+ this[`$s$${key}`]?.forEach(k => {
+ if (`$w$${k}` in this) {
+ // ---- Watcher
+ this[k](key);
+ } else if (`$md$${k}` in this) {
+ this[k]._$update();
+ } else {
+ // ---- Regular derived value
+ this[k] = this[`$f$${k}`];
+ }
+ });
+
+ // ---- "trigger-view"
+ this._$updateView(key);
+ }
+
+ _$updateView(key) {
+ if (this._$modelCallee) return this._$updateModelCallee();
+ if (!('_$update' in this)) return;
+ const depNum = this[`$$${key}`];
+ if (!depNum) return;
+ // ---- Collect all depNums that need to be updated
+ if ('_$depNumsToUpdate' in this) {
+ this._$depNumsToUpdate.push(depNum);
+ } else {
+ this._$depNumsToUpdate = [depNum];
+ // ---- Update in the next microtask
+ Promise.resolve().then(() => {
+ // ---- Abort if unmounted
+ if (this._$unmounted) return;
+ const depNums = this._$depNumsToUpdate;
+ if (depNums.length > 0) {
+ const depNum = depNums.reduce((acc, cur) => acc | cur, 0);
+ this._$update(depNum);
+ }
+ delete this._$depNumsToUpdate;
+ });
+ }
+ }
+
+ _$updateModelCallee() {
+ if ('_$depNumsToUpdate' in this) return;
+ this._$depNumsToUpdate = true;
+ // ---- Update in the next microtask
+ Promise.resolve().then(() => {
+ // ---- Abort if unmounted
+ if (this._$unmounted) return;
+ this._$modelCallee._$updateDerived(this._$modelKey);
+ delete this._$depNumsToUpdate;
+ });
+ }
+ /**
+ * @brief Update all props and content of the model
+ */
+ static _$updateModel(model, propsFunc, contentFunc) {
+ // ---- Suppress update because top level update will be performed
+ // directly by the state variable in the model callee, which will
+ // trigger the update of the model
+ const props = propsFunc() ?? {};
+ const collectedProps = props.s ?? [];
+ props.m?.forEach(([props, deps]) => {
+ Object.entries(props).forEach(([key, value]) => {
+ collectedProps.push([key, value, deps]);
+ });
+ });
+ collectedProps.forEach(([key, value, deps]) => {
+ model._$setProp(key, () => value, deps);
+ });
+ const content = contentFunc();
+ if (content) model._$setContent(() => content[0], content[1]);
+ }
+
+ static _$releaseModel() {
+ delete this._$modelCallee;
+ }
+
+ /**
+ * @brief Inject Dlight model in to a property
+ * @param ModelCls
+ * @param props { m: [props, deps], s: [key, value, deps] }
+ * @param content
+ * @param key
+ * @returns
+ */
+ _$injectModel(ModelCls, propsFunc, contentFunc, key) {
+ const props = propsFunc() ?? {};
+ const collectedProps = props.s ?? [];
+ props.m?.forEach(([props, deps]) => {
+ Object.entries(props).forEach(([key, value]) => {
+ collectedProps.push([key, value, deps]);
+ });
+ });
+ const model = new ModelCls();
+ model._$init(collectedProps, contentFunc(), null, null);
+ model._$modelCallee = this;
+ model._$modelKey = key;
+ model._$update = CompNode._$updateModel.bind(null, model, propsFunc, contentFunc);
+
+ return model;
+ }
+}
+
+// ---- @View -> class Comp extends View
+export const View = CompNode;
+export const Model = CompNode;
+
+/**
+ * @brief Run all update functions given the key
+ * @param dlNode
+ * @param key
+ */
+export function update(dlNode, key) {
+ dlNode._$updateDerived(key);
+}
diff --git a/packages/inula-v2/src/DLNode.js b/packages/inula-v2/src/DLNode.js
new file mode 100644
index 00000000..9fafd54b
--- /dev/null
+++ b/packages/inula-v2/src/DLNode.js
@@ -0,0 +1,209 @@
+import { DLStore } from './store';
+
+export const DLNodeType = {
+ Comp: 0,
+ For: 1,
+ Cond: 2,
+ Env: 3,
+ Exp: 4,
+ Snippet: 5,
+ Try: 6,
+};
+
+export class DLNode {
+ /**
+ * @brief Node type: HTML, Text, Custom, For, If, Env, Expression
+ */
+ _$dlNodeType;
+
+ /**
+ * @brief Constructor
+ * @param nodeType
+ */
+ constructor(nodeType) {
+ this._$dlNodeType = nodeType;
+ }
+
+ /**
+ * @brief Node element
+ * Either one real element for HTMLNode and TextNode
+ * Or an array of DLNode for CustomNode, ForNode, IfNode, EnvNode, ExpNode
+ */
+ get _$el() {
+ return DLNode.toEls(this._$nodes);
+ }
+
+ /**
+ * @brief Loop all child DLNodes to get all the child elements
+ * @param nodes
+ * @returns HTMLElement[]
+ */
+ static toEls(nodes) {
+ const els = [];
+ this.loopShallowEls(nodes, el => {
+ els.push(el);
+ });
+ return els;
+ }
+
+ // ---- Loop nodes ----
+ /**
+ * @brief Loop all elements shallowly,
+ * i.e., don't loop the child nodes of dom elements and only call runFunc on dom elements
+ * @param nodes
+ * @param runFunc
+ */
+ static loopShallowEls(nodes, runFunc) {
+ const stack = [...nodes].reverse();
+ while (stack.length > 0) {
+ const node = stack.pop();
+ if (!('_$dlNodeType' in node)) runFunc(node);
+ else node._$nodes && stack.push(...[...node._$nodes].reverse());
+ }
+ }
+
+ /**
+ * @brief Add parentEl to all nodes until the first element
+ * @param nodes
+ * @param parentEl
+ */
+ static addParentEl(nodes, parentEl) {
+ nodes.forEach(node => {
+ if ('_$dlNodeType' in node) {
+ node._$parentEl = parentEl;
+ node._$nodes && DLNode.addParentEl(node._$nodes, parentEl);
+ }
+ });
+ }
+
+ // ---- Flow index and add child elements ----
+ /**
+ * @brief Get the total count of dom elements before the stop node
+ * @param nodes
+ * @param stopNode
+ * @returns total count of dom elements
+ */
+ static getFlowIndexFromNodes(nodes, stopNode) {
+ let index = 0;
+ const stack = [...nodes].reverse();
+ while (stack.length > 0) {
+ const node = stack.pop();
+ if (node === stopNode) break;
+ if ('_$dlNodeType' in node) {
+ node._$nodes && stack.push(...[...node._$nodes].reverse());
+ } else {
+ index++;
+ }
+ }
+ return index;
+ }
+
+ /**
+ * @brief Given an array of nodes, append them to the parentEl
+ * 1. If nextSibling is provided, insert the nodes before the nextSibling
+ * 2. If nextSibling is not provided, append the nodes to the parentEl
+ * @param nodes
+ * @param parentEl
+ * @param nextSibling
+ * @returns Added element count
+ */
+ static appendNodesWithSibling(nodes, parentEl, nextSibling) {
+ if (nextSibling) return this.insertNodesBefore(nodes, parentEl, nextSibling);
+ return this.appendNodes(nodes, parentEl);
+ }
+
+ /**
+ * @brief Given an array of nodes, append them to the parentEl using the index
+ * 1. If the index is the same as the length of the parentEl.childNodes, append the nodes to the parentEl
+ * 2. If the index is not the same as the length of the parentEl.childNodes, insert the nodes before the node at the index
+ * @param nodes
+ * @param parentEl
+ * @param index
+ * @param length
+ * @returns Added element count
+ */
+ static appendNodesWithIndex(nodes, parentEl, index, length) {
+ length = length ?? parentEl.childNodes.length;
+ if (length !== index) return this.insertNodesBefore(nodes, parentEl, parentEl.childNodes[index]);
+ return this.appendNodes(nodes, parentEl);
+ }
+
+ /**
+ * @brief Insert nodes before the nextSibling
+ * @param nodes
+ * @param parentEl
+ * @param nextSibling
+ * @returns Added element count
+ */
+ static insertNodesBefore(nodes, parentEl, nextSibling) {
+ let count = 0;
+ this.loopShallowEls(nodes, el => {
+ parentEl.insertBefore(el, nextSibling);
+ count++;
+ });
+ return count;
+ }
+
+ /**
+ * @brief Append nodes to the parentEl
+ * @param nodes
+ * @param parentEl
+ * @returns Added element count
+ */
+ static appendNodes(nodes, parentEl) {
+ let count = 0;
+ this.loopShallowEls(nodes, el => {
+ parentEl.appendChild(el);
+ count++;
+ });
+ return count;
+ }
+
+ // ---- Lifecycle ----
+ /**
+ * @brief Add willUnmount function to node
+ * @param node
+ * @param func
+ */
+ static addWillUnmount(node, func) {
+ const willUnmountStore = DLStore.global.WillUnmountStore;
+ const currentStore = willUnmountStore[willUnmountStore.length - 1];
+ // ---- If the current store is empty, it means this node is not mutable
+ if (!currentStore) return;
+ currentStore.push(func.bind(null, node));
+ }
+
+ /**
+ * @brief Add didUnmount function to node
+ * @param node
+ * @param func
+ */
+ static addDidUnmount(node, func) {
+ const didUnmountStore = DLStore.global.DidUnmountStore;
+ const currentStore = didUnmountStore[didUnmountStore.length - 1];
+ // ---- If the current store is empty, it means this node is not mutable
+ if (!currentStore) return;
+ currentStore.push(func.bind(null, node));
+ }
+
+ /**
+ * @brief Add didUnmount function to global store
+ * @param func
+ */
+ static addDidMount(node, func) {
+ if (!DLStore.global.DidMountStore) DLStore.global.DidMountStore = [];
+ DLStore.global.DidMountStore.push(func.bind(null, node));
+ }
+
+ /**
+ * @brief Run all didMount functions and reset the global store
+ */
+ static runDidMount() {
+ const didMountStore = DLStore.global.DidMountStore;
+ if (!didMountStore || didMountStore.length === 0) return;
+ for (let i = didMountStore.length - 1; i >= 0; i--) {
+ didMountStore[i]();
+ }
+ DLStore.global.DidMountStore = [];
+ }
+}
diff --git a/packages/inula-v2/src/EnvNode.js b/packages/inula-v2/src/EnvNode.js
new file mode 100644
index 00000000..22b1b4c0
--- /dev/null
+++ b/packages/inula-v2/src/EnvNode.js
@@ -0,0 +1,103 @@
+import { DLNode, DLNodeType } from './DLNode';
+import { DLStore, cached } from './store';
+
+export class EnvStoreClass {
+ constructor() {
+ this.envs = {};
+ this.currentEnvNodes = [];
+ }
+
+ /**
+ * @brief Add a node to the current env and merge envs
+ * @param node - The node to add
+ */
+ addEnvNode(node) {
+ this.currentEnvNodes.push(node);
+ this.mergeEnvs();
+ }
+
+ /**
+ * @brief Replace the current env with the given nodes and merge envs
+ * @param nodes - The nodes to replace the current environment with
+ */
+ replaceEnvNodes(nodes) {
+ this.currentEnvNodes = nodes;
+ this.mergeEnvs();
+ }
+
+ /**
+ * @brief Remove the last node from the current env and merge envs
+ */
+ removeEnvNode() {
+ this.currentEnvNodes.pop();
+ this.mergeEnvs();
+ }
+
+ /**
+ * @brief Merge all the envs in currentEnvNodes, inner envs override outer envs
+ */
+ mergeEnvs() {
+ this.envs = {};
+ this.currentEnvNodes.forEach(envNode => {
+ Object.entries(envNode.envs).forEach(([key, value]) => {
+ this.envs[key] = [value, envNode];
+ });
+ });
+ }
+}
+
+export class EnvNode extends DLNode {
+ constructor(envs, depsArr) {
+ super(DLNodeType.Env);
+ // Declare a global variable to store the environment variables
+ if (!('DLEnvStore' in DLStore.global)) DLStore.global.DLEnvStore = new EnvStoreClass();
+
+ this.envs = envs;
+ this.depsArr = depsArr;
+ this.updateNodes = new Set();
+
+ DLStore.global.DLEnvStore.addEnvNode(this);
+ }
+
+ cached(deps, name) {
+ if (!deps || !deps.length) return false;
+ if (cached(deps, this.depsArr[name])) return true;
+ this.depsArr[name] = deps;
+ return false;
+ }
+
+ /**
+ * @brief Update a specific env, and update all the comp nodes that depend on this env
+ * @param name - The name of the environment variable to update
+ * @param value - The new value of the environment variable
+ */
+ updateEnv(name, valueFunc, deps) {
+ if (this.cached(deps, name)) return;
+ const value = valueFunc();
+ this.envs[name] = value;
+ if (DLStore.global.DLEnvStore.currentEnvNodes.includes(this)) {
+ DLStore.global.DLEnvStore.mergeEnvs();
+ }
+ this.updateNodes.forEach(node => {
+ node._$updateEnv(name, value, this);
+ });
+ }
+
+ /**
+ * @brief Add a node to this.updateNodes, delete the node from this.updateNodes when it unmounts
+ * @param node - The node to add
+ */
+ addNode(node) {
+ this.updateNodes.add(node);
+ DLNode.addWillUnmount(node, this.updateNodes.delete.bind(this.updateNodes, node));
+ }
+
+ /**
+ * @brief Set this._$nodes, and exit the current env
+ * @param nodes - The nodes to set
+ */
+ initNodes(nodes) {
+ this._$nodes = nodes;
+ DLStore.global.DLEnvStore.removeEnvNode();
+ }
+}
diff --git a/packages/inula-v2/src/HTMLNode.js b/packages/inula-v2/src/HTMLNode.js
new file mode 100644
index 00000000..f8f8ab4a
--- /dev/null
+++ b/packages/inula-v2/src/HTMLNode.js
@@ -0,0 +1,163 @@
+import { DLNode } from './DLNode';
+import { DLStore, cached } from './store';
+
+function cache(el, key, deps) {
+ if (deps.length === 0) return false;
+ const cacheKey = `$${key}`;
+ if (cached(deps, el[cacheKey])) return true;
+ el[cacheKey] = deps;
+ return false;
+}
+
+/**
+ * @brief Plainly set style
+ * @param el
+ * @param value
+ */
+export function setStyle(el, value) {
+ Object.entries(value).forEach(([key, value]) => {
+ if (key.startsWith('--')) {
+ el.style.setProperty(key, value);
+ } else {
+ el.style[key] = value;
+ }
+ });
+}
+
+/**
+ * @brief Plainly set dataset
+ * @param el
+ * @param value
+ */
+export function setDataset(el, value) {
+ Object.assign(el.dataset, value);
+}
+
+/**
+ * @brief Set HTML property with checking value equality first
+ * @param el
+ * @param key
+ * @param value
+ */
+export function setHTMLProp(el, key, valueFunc, deps) {
+ // ---- Comparing deps, same value won't trigger
+ // will lead to a bug if the value is set outside of the DLNode
+ // e.g. setHTMLProp(el, "textContent", "value", [])
+ // => el.textContent = "other"
+ // => setHTMLProp(el, "textContent", "value", [])
+ // The value will be set to "other" instead of "value"
+ if (cache(el, key, deps)) return;
+ el[key] = valueFunc();
+}
+
+/**
+ * @brief Plainly set HTML properties
+ * @param el
+ * @param value
+ */
+export function setHTMLProps(el, value) {
+ Object.entries(value).forEach(([key, v]) => {
+ if (key === 'style') return setStyle(el, v);
+ if (key === 'dataset') return setDataset(el, v);
+ setHTMLProp(el, key, () => v, []);
+ });
+}
+
+/**
+ * @brief Set HTML attribute with checking value equality first
+ * @param el
+ * @param key
+ * @param value
+ */
+export function setHTMLAttr(el, key, valueFunc, deps) {
+ if (cache(el, key, deps)) return;
+ el.setAttribute(key, valueFunc());
+}
+
+/**
+ * @brief Plainly set HTML attributes
+ * @param el
+ * @param value
+ */
+export function setHTMLAttrs(el, value) {
+ Object.entries(value).forEach(([key, v]) => {
+ setHTMLAttr(el, key, () => v, []);
+ });
+}
+
+/**
+ * @brief Set memorized event, store the previous event in el[`$on${key}`], if it exists, remove it first
+ * @param el
+ * @param key
+ * @param value
+ */
+export function setEvent(el, key, value) {
+ const prevEvent = el[`$on${key}`];
+ if (prevEvent) el.removeEventListener(key, prevEvent);
+ el.addEventListener(key, value);
+ el[`$on${key}`] = value;
+}
+
+function eventHandler(e) {
+ const key = `$$${e.type}`;
+ for (const node of e.composedPath()) {
+ if (node[key]) node[key](e);
+ if (e.cancelBubble) return;
+ }
+}
+
+export function delegateEvent(el, key, value) {
+ if (el[`$$${key}`] === value) return;
+ el[`$$${key}`] = value;
+ if (!DLStore.delegatedEvents.has(key)) {
+ DLStore.delegatedEvents.add(key);
+ DLStore.document.addEventListener(key, eventHandler);
+ }
+}
+/**
+ * @brief Shortcut for document.createElement
+ * @param tag
+ * @returns HTMLElement
+ */
+export function createElement(tag) {
+ return DLStore.document.createElement(tag);
+}
+
+/**
+ * @brief Insert any DLNode into an element, set the _$nodes and append the element to the element's children
+ * @param el
+ * @param node
+ * @param position
+ */
+export function insertNode(el, node, position) {
+ // ---- Set _$nodes
+ if (!el._$nodes) el._$nodes = Array.from(el.childNodes);
+ el._$nodes.splice(position, 0, node);
+
+ // ---- Insert nodes' elements
+ const flowIdx = DLNode.getFlowIndexFromNodes(el._$nodes, node);
+ DLNode.appendNodesWithIndex([node], el, flowIdx);
+ // ---- Set parentEl
+ DLNode.addParentEl([node], el);
+}
+
+/**
+ * @brief An inclusive assign prop function that accepts any type of prop
+ * @param el
+ * @param key
+ * @param value
+ */
+export function forwardHTMLProp(el, key, valueFunc, deps) {
+ if (key === 'style') return setStyle(el, valueFunc());
+ if (key === 'dataset') return setDataset(el, valueFunc());
+ if (key === 'element') return;
+ if (key === 'prop') return setHTMLProps(el, valueFunc());
+ if (key === 'attr') return setHTMLAttrs(el, valueFunc());
+ if (key === 'innerHTML') return setHTMLProp(el, 'innerHTML', valueFunc, deps);
+ if (key === 'textContent') return setHTMLProp(el, 'textContent', valueFunc, deps);
+ if (key === 'forwardProp') return;
+ if (key.startsWith('on')) {
+ return setEvent(el, key.slice(2).toLowerCase(), valueFunc());
+ }
+ setHTMLAttr(el, key, valueFunc, deps);
+}
diff --git a/packages/inula-v2/src/MutableNode/CondNode.js b/packages/inula-v2/src/MutableNode/CondNode.js
new file mode 100644
index 00000000..dbf75f48
--- /dev/null
+++ b/packages/inula-v2/src/MutableNode/CondNode.js
@@ -0,0 +1,69 @@
+import { DLNodeType } from '../DLNode';
+import { FlatNode } from './FlatNode';
+
+export class CondNode extends FlatNode {
+ /**
+ * @brief Constructor, If type, accept a function that returns a list of nodes
+ * @param caseFunc
+ */
+ constructor(depNum, condFunc) {
+ super(DLNodeType.Cond);
+ this.depNum = depNum;
+ this.cond = -1;
+ this.condFunc = condFunc;
+ this.initUnmountStore();
+ this._$nodes = this.condFunc(this);
+ this.setUnmountFuncs();
+
+ // ---- Add to the global UnmountStore
+ CondNode.addWillUnmount(this, this.runWillUnmount.bind(this));
+ CondNode.addDidUnmount(this, this.runDidUnmount.bind(this));
+ }
+
+ /**
+ * @brief Update the nodes in the environment
+ */
+ updateCond(key) {
+ // ---- Need to save prev unmount funcs because we can't put removeNodes before geneNewNodesInEnv
+ // The reason is that if it didn't change, we don't need to unmount or remove the nodes
+ const prevFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
+ const newNodes = this.geneNewNodesInEnv(() => this.condFunc(this));
+
+ // ---- If the new nodes are the same as the old nodes, we only need to update children
+ if (this.didntChange) {
+ [this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
+ this.didntChange = false;
+ this.updateFunc?.(this.depNum, key);
+ return;
+ }
+ // ---- Remove old nodes
+ const newFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
+ [this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
+ this._$nodes && this._$nodes.length > 0 && this.removeNodes(this._$nodes);
+ [this.willUnmountFuncs, this.didUnmountFuncs] = newFuncs;
+
+ if (newNodes.length === 0) {
+ // ---- No branch has been taken
+ this._$nodes = [];
+ return;
+ }
+ // ---- Add new nodes
+ const parentEl = this._$parentEl;
+ // ---- Faster append with nextSibling rather than flowIndex
+ const flowIndex = CondNode.getFlowIndexFromNodes(parentEl._$nodes, this);
+
+ const nextSibling = parentEl.childNodes[flowIndex];
+ CondNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
+ CondNode.runDidMount();
+ this._$nodes = newNodes;
+ }
+
+ /**
+ * @brief The update function of IfNode's childNodes is stored in the first child node
+ * @param changed
+ */
+ update(changed) {
+ if (!(~this.depNum & changed)) return;
+ this.updateFunc?.(changed);
+ }
+}
diff --git a/packages/inula-v2/src/MutableNode/ExpNode.js b/packages/inula-v2/src/MutableNode/ExpNode.js
new file mode 100644
index 00000000..0373dc1f
--- /dev/null
+++ b/packages/inula-v2/src/MutableNode/ExpNode.js
@@ -0,0 +1,86 @@
+import { DLNodeType } from '../DLNode';
+import { FlatNode } from './FlatNode';
+import { DLStore, cached } from '../store';
+
+export class ExpNode extends FlatNode {
+ /**
+ * @brief Constructor, Exp type, accept a function that returns a list of nodes
+ * @param nodesFunc
+ */
+ constructor(value, deps) {
+ super(DLNodeType.Exp);
+ this.initUnmountStore();
+ this._$nodes = ExpNode.formatNodes(value);
+ this.setUnmountFuncs();
+ this.deps = this.parseDeps(deps);
+ // ---- Add to the global UnmountStore
+ ExpNode.addWillUnmount(this, this.runWillUnmount.bind(this));
+ ExpNode.addDidUnmount(this, this.runDidUnmount.bind(this));
+ }
+
+ parseDeps(deps) {
+ return deps.map(dep => {
+ // ---- CompNode
+ if (dep?.prototype?._$init) return dep.toString();
+ // ---- SnippetNode
+ if (dep?.propViewFunc) return dep.propViewFunc.toString();
+ return dep;
+ });
+ }
+
+ cache(deps) {
+ if (!deps || !deps.length) return false;
+ deps = this.parseDeps(deps);
+ if (cached(deps, this.deps)) return true;
+ this.deps = deps;
+ return false;
+ }
+ /**
+ * @brief Generate new nodes and replace the old nodes
+ */
+ update(valueFunc, deps) {
+ if (this.cache(deps)) return;
+ this.removeNodes(this._$nodes);
+ const newNodes = this.geneNewNodesInEnv(() => ExpNode.formatNodes(valueFunc()));
+ if (newNodes.length === 0) {
+ this._$nodes = [];
+ return;
+ }
+
+ // ---- Add new nodes
+ const parentEl = this._$parentEl;
+ const flowIndex = ExpNode.getFlowIndexFromNodes(parentEl._$nodes, this);
+ const nextSibling = parentEl.childNodes[flowIndex];
+ ExpNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
+ ExpNode.runDidMount();
+
+ this._$nodes = newNodes;
+ }
+
+ /**
+ * @brief Format the nodes
+ * @param nodes
+ * @returns New nodes
+ */
+ static formatNodes(nodes) {
+ if (!Array.isArray(nodes)) nodes = [nodes];
+ return (
+ nodes
+ // ---- Flatten the nodes
+ .flat(1)
+ // ---- Filter out empty nodes
+ .filter(node => node !== undefined && node !== null && typeof node !== 'boolean')
+ .map(node => {
+ // ---- If the node is a string, number or bigint, convert it to a text node
+ if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
+ return DLStore.document.createTextNode(`${node}`);
+ }
+ // ---- If the node has PropView, call it to get the view
+ if ('propViewFunc' in node) return node.build();
+ return node;
+ })
+ // ---- Flatten the nodes again
+ .flat(1)
+ );
+ }
+}
diff --git a/packages/inula-v2/src/MutableNode/FlatNode.js b/packages/inula-v2/src/MutableNode/FlatNode.js
new file mode 100644
index 00000000..9f1519e8
--- /dev/null
+++ b/packages/inula-v2/src/MutableNode/FlatNode.js
@@ -0,0 +1,33 @@
+import { DLStore } from '../store';
+import { MutableNode } from './MutableNode';
+
+export class FlatNode extends MutableNode {
+ willUnmountFuncs = [];
+ didUnmountFuncs = [];
+
+ setUnmountFuncs() {
+ this.willUnmountFuncs = DLStore.global.WillUnmountStore.pop();
+ this.didUnmountFuncs = DLStore.global.DidUnmountStore.pop();
+ }
+
+ runWillUnmount() {
+ for (let i = 0; i < this.willUnmountFuncs.length; i++) this.willUnmountFuncs[i]();
+ }
+
+ runDidUnmount() {
+ for (let i = this.didUnmountFuncs.length - 1; i >= 0; i--) this.didUnmountFuncs[i]();
+ }
+
+ removeNodes(nodes) {
+ this.runWillUnmount();
+ super.removeNodes(nodes);
+ this.runDidUnmount();
+ }
+
+ geneNewNodesInEnv(newNodesFunc) {
+ this.initUnmountStore();
+ const nodes = super.geneNewNodesInEnv(newNodesFunc);
+ this.setUnmountFuncs();
+ return nodes;
+ }
+}
diff --git a/packages/inula-v2/src/MutableNode/ForNode.js b/packages/inula-v2/src/MutableNode/ForNode.js
new file mode 100644
index 00000000..0cc14972
--- /dev/null
+++ b/packages/inula-v2/src/MutableNode/ForNode.js
@@ -0,0 +1,406 @@
+import { DLNodeType } from '../DLNode';
+import { DLStore } from '../store';
+import { MutableNode } from './MutableNode';
+
+export class ForNode extends MutableNode {
+ array;
+ nodeFunc;
+ depNum;
+
+ nodesMap = new Map();
+ updateArr = [];
+
+ /**
+ * @brief Getter for nodes
+ */
+ get _$nodes() {
+ const nodes = [];
+ for (let idx = 0; idx < this.array.length; idx++) {
+ nodes.push(...this.nodesMap.get(this.keys?.[idx] ?? idx));
+ }
+ return nodes;
+ }
+
+ /**
+ * @brief Constructor, For type
+ * @param array
+ * @param nodeFunc
+ * @param keys
+ */
+ constructor(array, depNum, keys, nodeFunc) {
+ super(DLNodeType.For);
+ this.array = [...array];
+ this.keys = keys;
+ this.depNum = depNum;
+ this.addNodeFunc(nodeFunc);
+ }
+
+ /**
+ * @brief To be called immediately after the constructor
+ * @param nodeFunc
+ */
+ addNodeFunc(nodeFunc) {
+ this.nodeFunc = nodeFunc;
+ this.array.forEach((item, idx) => {
+ this.initUnmountStore();
+ const key = this.keys?.[idx] ?? idx;
+ const nodes = nodeFunc(item, this.updateArr, idx);
+ this.nodesMap.set(key, nodes);
+ this.setUnmountMap(key);
+ });
+ // ---- For nested ForNode, the whole strategy is just like EnvStore
+ // we use array of function array to create "environment", popping and pushing
+ ForNode.addWillUnmount(this, this.runAllWillUnmount.bind(this));
+ ForNode.addDidUnmount(this, this.runAllDidUnmount.bind(this));
+ }
+
+ /**
+ * @brief Update the view related to one item in the array
+ * @param nodes
+ * @param item
+ */
+ updateItem(idx, array, changed) {
+ // ---- The update function of ForNode's childNodes is stored in the first child node
+ this.updateArr[idx]?.(changed ?? this.depNum, array[idx]);
+ }
+
+ updateItems(changed) {
+ for (let idx = 0; idx < this.array.length; idx++) {
+ this.updateItem(idx, this.array, changed);
+ }
+ }
+
+ /**
+ * @brief Non-array update function
+ * @param changed
+ */
+ update(changed) {
+ // ---- e.g. this.depNum -> 1110 changed-> 1010
+ // ~this.depNum & changed -> ~1110 & 1010 -> 0000
+ // no update because depNum contains all the changed
+ // ---- e.g. this.depNum -> 1110 changed-> 1101
+ // ~this.depNum & changed -> ~1110 & 1101 -> 0001
+ // update because depNum doesn't contain all the changed
+ if (!(~this.depNum & changed)) return;
+ this.updateItems(changed);
+ }
+
+ /**
+ * @brief Array-related update function
+ * @param newArray
+ * @param newKeys
+ */
+ updateArray(newArray, newKeys) {
+ if (newKeys) {
+ this.updateWithKey(newArray, newKeys);
+ return;
+ }
+ this.updateWithOutKey(newArray);
+ }
+
+ /**
+ * @brief Shortcut to generate new nodes with idx and key
+ */
+ getNewNodes(idx, key, array, updateArr) {
+ this.initUnmountStore();
+ const nodes = this.geneNewNodesInEnv(() => this.nodeFunc(array[idx], updateArr ?? this.updateArr, idx));
+ this.setUnmountMap(key);
+ this.nodesMap.set(key, nodes);
+ return nodes;
+ }
+
+ /**
+ * @brief Set the unmount map by getting the last unmount map from the global store
+ * @param key
+ */
+ setUnmountMap(key) {
+ const willUnmountMap = DLStore.global.WillUnmountStore.pop();
+ if (willUnmountMap && willUnmountMap.length > 0) {
+ if (!this.willUnmountMap) this.willUnmountMap = new Map();
+ this.willUnmountMap.set(key, willUnmountMap);
+ }
+ const didUnmountMap = DLStore.global.DidUnmountStore.pop();
+ if (didUnmountMap && didUnmountMap.length > 0) {
+ if (!this.didUnmountMap) this.didUnmountMap = new Map();
+ this.didUnmountMap.set(key, didUnmountMap);
+ }
+ }
+
+ /**
+ * @brief Run all the unmount functions and clear the unmount map
+ */
+ runAllWillUnmount() {
+ if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
+ this.willUnmountMap.forEach(funcs => {
+ for (let i = 0; i < funcs.length; i++) funcs[i]?.();
+ });
+ this.willUnmountMap.clear();
+ }
+
+ /**
+ * @brief Run all the unmount functions and clear the unmount map
+ */
+ runAllDidUnmount() {
+ if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
+ this.didUnmountMap.forEach(funcs => {
+ for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
+ });
+ this.didUnmountMap.clear();
+ }
+
+ /**
+ * @brief Run the unmount functions of the given key
+ * @param key
+ */
+ runWillUnmount(key) {
+ if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
+ const funcs = this.willUnmountMap.get(key);
+ if (!funcs) return;
+ for (let i = 0; i < funcs.length; i++) funcs[i]?.();
+ this.willUnmountMap.delete(key);
+ }
+
+ /**
+ * @brief Run the unmount functions of the given key
+ */
+ runDidUnmount(key) {
+ if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
+ const funcs = this.didUnmountMap.get(key);
+ if (!funcs) return;
+ for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
+ this.didUnmountMap.delete(key);
+ }
+
+ /**
+ * @brief Remove nodes from parentEl and run willUnmount and didUnmount
+ * @param nodes
+ * @param key
+ */
+ removeNodes(nodes, key) {
+ this.runWillUnmount(key);
+ super.removeNodes(nodes);
+ this.runDidUnmount(key);
+ this.nodesMap.delete(key);
+ }
+
+ /**
+ * @brief Update the nodes without keys
+ * @param newArray
+ */
+ updateWithOutKey(newArray) {
+ const preLength = this.array.length;
+ const currLength = newArray.length;
+
+ if (preLength === currLength) {
+ // ---- If the length is the same, we only need to update the nodes
+ for (let idx = 0; idx < this.array.length; idx++) {
+ this.updateItem(idx, newArray);
+ }
+ this.array = [...newArray];
+ return;
+ }
+ const parentEl = this._$parentEl;
+ // ---- If the new array is longer, add new nodes directly
+ if (preLength < currLength) {
+ let flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
+ // ---- Calling parentEl.childNodes.length is time-consuming,
+ // so we use a length variable to store the length
+ const length = parentEl.childNodes.length;
+ for (let idx = 0; idx < currLength; idx++) {
+ if (idx < preLength) {
+ flowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(idx));
+ this.updateItem(idx, newArray);
+ continue;
+ }
+ const newNodes = this.getNewNodes(idx, idx, newArray);
+ ForNode.appendNodesWithIndex(newNodes, parentEl, flowIndex, length);
+ }
+ ForNode.runDidMount();
+ this.array = [...newArray];
+ return;
+ }
+
+ // ---- Update the nodes first
+ for (let idx = 0; idx < currLength; idx++) {
+ this.updateItem(idx, newArray);
+ }
+ // ---- If the new array is shorter, remove the extra nodes
+ for (let idx = currLength; idx < preLength; idx++) {
+ const nodes = this.nodesMap.get(idx);
+ this.removeNodes(nodes, idx);
+ }
+ this.updateArr.splice(currLength, preLength - currLength);
+ this.array = [...newArray];
+ }
+
+ /**
+ * @brief Update the nodes with keys
+ * @param newArray
+ * @param newKeys
+ */
+ updateWithKey(newArray, newKeys) {
+ if (newKeys.length !== new Set(newKeys).size) {
+ throw new Error('DLight: Duplicate keys in for loop are not allowed');
+ }
+ const prevKeys = this.keys;
+ this.keys = newKeys;
+
+ if (ForNode.arrayEqual(prevKeys, this.keys)) {
+ // ---- If the keys are the same, we only need to update the nodes
+ for (let idx = 0; idx < newArray.length; idx++) {
+ this.updateItem(idx, newArray);
+ }
+ this.array = [...newArray];
+ return;
+ }
+
+ const parentEl = this._$parentEl;
+
+ // ---- No nodes after, delete all nodes
+ if (this.keys.length === 0) {
+ const parentNodes = parentEl._$nodes ?? [];
+ if (parentNodes.length === 1 && parentNodes[0] === this) {
+ // ---- ForNode is the only node in the parent node
+ // Frequently used in real life scenarios because we tend to always wrap for with a div element,
+ // so we optimize it here
+ this.runAllWillUnmount();
+ parentEl.innerHTML = '';
+ this.runAllDidUnmount();
+ } else {
+ for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
+ const prevKey = prevKeys[prevIdx];
+ this.removeNodes(this.nodesMap.get(prevKey), prevKey);
+ }
+ }
+ this.nodesMap.clear();
+ this.updateArr = [];
+ this.array = [];
+ return;
+ }
+
+ // ---- Record how many nodes are before this ForNode with the same parentNode
+ const flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
+
+ // ---- No nodes before, append all nodes
+ if (prevKeys.length === 0) {
+ const nextSibling = parentEl.childNodes[flowIndex];
+ for (let idx = 0; idx < this.keys.length; idx++) {
+ const newNodes = this.getNewNodes(idx, this.keys[idx], newArray);
+ ForNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
+ }
+ ForNode.runDidMount();
+ this.array = [...newArray];
+ return;
+ }
+
+ const shuffleKeys = [];
+ const newUpdateArr = [];
+
+ // ---- 1. Delete the nodes that are no longer in the array
+ for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
+ const prevKey = prevKeys[prevIdx];
+ if (this.keys.includes(prevKey)) {
+ shuffleKeys.push(prevKey);
+ newUpdateArr.push(this.updateArr[prevIdx]);
+ continue;
+ }
+ this.removeNodes(this.nodesMap.get(prevKey), prevKey);
+ }
+
+ // ---- 2. Add the nodes that are not in the array but in the new array
+ // ---- Calling parentEl.childNodes.length is time-consuming,
+ // so we use a length variable to store the length
+ let length = parentEl.childNodes.length;
+ let newFlowIndex = flowIndex;
+ for (let idx = 0; idx < this.keys.length; idx++) {
+ const key = this.keys[idx];
+ const prevIdx = shuffleKeys.indexOf(key);
+ if (prevIdx !== -1) {
+ // ---- These nodes are already in the parentEl,
+ // and we need to keep track of their flowIndex
+ newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
+ newUpdateArr[prevIdx]?.(this.depNum, newArray[idx]);
+ continue;
+ }
+ // ---- Insert updateArr first because in getNewNode the updateFunc will replace this null
+ newUpdateArr.splice(idx, 0, null);
+ const newNodes = this.getNewNodes(idx, key, newArray, newUpdateArr);
+ // ---- Add the new nodes
+ shuffleKeys.splice(idx, 0, key);
+
+ const count = ForNode.appendNodesWithIndex(newNodes, parentEl, newFlowIndex, length);
+ newFlowIndex += count;
+ length += count;
+ }
+ ForNode.runDidMount();
+
+ // ---- After adding and deleting, the only thing left is to reorder the nodes,
+ // but if the keys are the same, we don't need to reorder
+ if (ForNode.arrayEqual(this.keys, shuffleKeys)) {
+ this.array = [...newArray];
+ this.updateArr = newUpdateArr;
+ return;
+ }
+
+ newFlowIndex = flowIndex;
+ const bufferNodes = new Map();
+ // ---- 3. Replace the nodes in the same position using Fisher-Yates shuffle algorithm
+ for (let idx = 0; idx < this.keys.length; idx++) {
+ const key = this.keys[idx];
+ const prevIdx = shuffleKeys.indexOf(key);
+
+ const bufferedNode = bufferNodes.get(key);
+ if (bufferedNode) {
+ // ---- We need to add the flowIndex of the bufferedNode,
+ // because the bufferedNode is in the parentEl and the new position is ahead of the previous position
+ const bufferedFlowIndex = ForNode.getFlowIndexFromNodes(bufferedNode);
+ const lastEl = ForNode.toEls(bufferedNode).pop();
+ const nextSibling = parentEl.childNodes[newFlowIndex + bufferedFlowIndex];
+ if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
+ // ---- If the node is buffered, we need to add it to the parentEl
+ ForNode.insertNodesBefore(bufferedNode, parentEl, nextSibling);
+ }
+ // ---- So the added length is the length of the bufferedNode
+ newFlowIndex += bufferedFlowIndex;
+ delete bufferNodes[idx];
+ } else if (prevIdx === idx) {
+ // ---- If the node is in the same position, we don't need to do anything
+ newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
+ continue;
+ } else {
+ // ---- If the node is not in the same position, we need to buffer it
+ // We buffer the node of the previous position, and then replace it with the node of the current position
+ const prevKey = shuffleKeys[idx];
+ bufferNodes.set(prevKey, this.nodesMap.get(prevKey));
+ // ---- Length would never change, and the last will always be in the same position,
+ // so it'll always be insertBefore instead of appendChild
+ const childNodes = this.nodesMap.get(key);
+ const lastEl = ForNode.toEls(childNodes).pop();
+ const nextSibling = parentEl.childNodes[newFlowIndex];
+ if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
+ newFlowIndex += ForNode.insertNodesBefore(childNodes, parentEl, nextSibling);
+ }
+ }
+ // ---- Swap the keys
+ const tempKey = shuffleKeys[idx];
+ shuffleKeys[idx] = shuffleKeys[prevIdx];
+ shuffleKeys[prevIdx] = tempKey;
+ const tempUpdateFunc = newUpdateArr[idx];
+ newUpdateArr[idx] = newUpdateArr[prevIdx];
+ newUpdateArr[prevIdx] = tempUpdateFunc;
+ }
+ this.array = [...newArray];
+ this.updateArr = newUpdateArr;
+ }
+
+ /**
+ * @brief Compare two arrays
+ * @param arr1
+ * @param arr2
+ * @returns
+ */
+ static arrayEqual(arr1, arr2) {
+ if (arr1.length !== arr2.length) return false;
+ return arr1.every((item, idx) => item === arr2[idx]);
+ }
+}
diff --git a/packages/inula-v2/src/MutableNode/MutableNode.js b/packages/inula-v2/src/MutableNode/MutableNode.js
new file mode 100644
index 00000000..b557bdbc
--- /dev/null
+++ b/packages/inula-v2/src/MutableNode/MutableNode.js
@@ -0,0 +1,71 @@
+import { DLNode } from '../DLNode';
+import { DLStore } from '../store';
+
+export class MutableNode extends DLNode {
+ /**
+ * @brief Mutable node is a node that this._$nodes can be changed, things need to pay attention:
+ * 1. The environment of the new nodes should be the same as the old nodes
+ * 2. The new nodes should be added to the parentEl
+ * 3. The old nodes should be removed from the parentEl
+ * @param type
+ */
+ constructor(type) {
+ super(type);
+ // ---- Save the current environment nodes, must be a new reference
+ if (DLStore.global.DLEnvStore && DLStore.global.DLEnvStore.currentEnvNodes.length > 0) {
+ this.savedEnvNodes = [...DLStore.global.DLEnvStore.currentEnvNodes];
+ }
+ }
+
+ /**
+ * @brief Initialize the new nodes, add parentEl to all nodes
+ * @param nodes
+ */
+ initNewNodes(nodes) {
+ // ---- Add parentEl to all nodes
+ DLNode.addParentEl(nodes, this._$parentEl);
+ }
+
+ /**
+ * @brief Generate new nodes in the saved environment
+ * @param newNodesFunc
+ * @returns
+ */
+ geneNewNodesInEnv(newNodesFunc) {
+ if (!this.savedEnvNodes) {
+ // ---- No saved environment, just generate new nodes
+ const newNodes = newNodesFunc();
+ // ---- Only for IfNode's same condition return
+ // ---- Initialize the new nodes
+ this.initNewNodes(newNodes);
+ return newNodes;
+ }
+ // ---- Save the current environment nodes
+ const currentEnvNodes = DLStore.global.DLEnvStore.currentEnvNodes;
+ // ---- Replace the saved environment nodes
+ DLStore.global.DLEnvStore.replaceEnvNodes(this.savedEnvNodes);
+ const newNodes = newNodesFunc();
+ // ---- Retrieve the current environment nodes
+ DLStore.global.DLEnvStore.replaceEnvNodes(currentEnvNodes);
+ // ---- Only for IfNode's same condition return
+ // ---- Initialize the new nodes
+ this.initNewNodes(newNodes);
+ return newNodes;
+ }
+
+ initUnmountStore() {
+ DLStore.global.WillUnmountStore.push([]);
+ DLStore.global.DidUnmountStore.push([]);
+ }
+
+ /**
+ * @brief Remove nodes from parentEl and run willUnmount and didUnmount
+ * @param nodes
+ * @param removeEl Only remove outermost element
+ */
+ removeNodes(nodes) {
+ DLNode.loopShallowEls(nodes, node => {
+ this._$parentEl.removeChild(node);
+ });
+ }
+}
diff --git a/packages/inula-v2/src/MutableNode/TryNode.js b/packages/inula-v2/src/MutableNode/TryNode.js
new file mode 100644
index 00000000..12f1bb18
--- /dev/null
+++ b/packages/inula-v2/src/MutableNode/TryNode.js
@@ -0,0 +1,45 @@
+import { DLNodeType } from '../DLNode';
+import { FlatNode } from './FlatNode';
+import { EnvNode } from '../EnvNode';
+
+export class TryNode extends FlatNode {
+ constructor(tryFunc, catchFunc) {
+ super(DLNodeType.Try);
+ this.tryFunc = tryFunc;
+ const catchable = this.getCatchable(catchFunc);
+ this.envNode = new EnvNode({ _$catchable: catchable });
+ const nodes = tryFunc(this.setUpdateFunc.bind(this), catchable) ?? [];
+ this.envNode.initNodes(nodes);
+ this._$nodes = nodes;
+ }
+
+ update(changed) {
+ this.updateFunc?.(changed);
+ }
+
+ setUpdateFunc(updateFunc) {
+ this.updateFunc = updateFunc;
+ }
+
+ getCatchable(catchFunc) {
+ return callback =>
+ (...args) => {
+ try {
+ return callback(...args);
+ } catch (e) {
+ // ---- Run it in next tick to make sure when error occurs before
+ // didMount, this._$parentEl is not null
+ Promise.resolve().then(() => {
+ const nodes = this.geneNewNodesInEnv(() => catchFunc(this.setUpdateFunc.bind(this), e));
+ this._$nodes && this.removeNodes(this._$nodes);
+ const parentEl = this._$parentEl;
+ const flowIndex = FlatNode.getFlowIndexFromNodes(parentEl._$nodes, this);
+ const nextSibling = parentEl.childNodes[flowIndex];
+ FlatNode.appendNodesWithSibling(nodes, parentEl, nextSibling);
+ FlatNode.runDidMount();
+ this._$nodes = nodes;
+ });
+ }
+ };
+ }
+}
diff --git a/packages/inula-v2/src/PropView.js b/packages/inula-v2/src/PropView.js
new file mode 100644
index 00000000..2cb1701f
--- /dev/null
+++ b/packages/inula-v2/src/PropView.js
@@ -0,0 +1,48 @@
+import { DLNode } from './DLNode';
+import { insertNode } from './HTMLNode';
+export class PropView {
+ propViewFunc;
+ dlUpdateFunc = new Set();
+
+ /**
+ * @brief PropView constructor, accept a function that returns a list of DLNode
+ * @param propViewFunc - A function that when called, collects and returns an array of DLNode instances
+ */
+ constructor(propViewFunc) {
+ this.propViewFunc = propViewFunc;
+ }
+
+ /**
+ * @brief Build the prop view by calling the propViewFunc and add every single instance of the returned DLNode to dlUpdateNodes
+ * @returns An array of DLNode instances returned by propViewFunc
+ */
+ build() {
+ let update;
+ const addUpdate = updateFunc => {
+ update = updateFunc;
+ this.dlUpdateFunc.add(updateFunc);
+ };
+ const newNodes = this.propViewFunc(addUpdate);
+ if (newNodes.length === 0) return [];
+ if (update) {
+ // Remove the updateNode from dlUpdateNodes when it unmounts
+ DLNode.addWillUnmount(newNodes[0], this.dlUpdateFunc.delete.bind(this.dlUpdateFunc, update));
+ }
+
+ return newNodes;
+ }
+
+ /**
+ * @brief Update every node in dlUpdateNodes
+ * @param changed - A parameter indicating what changed to trigger the update
+ */
+ update(...args) {
+ this.dlUpdateFunc.forEach(update => {
+ update(...args);
+ });
+ }
+}
+
+export function insertChildren(el, propView) {
+ insertNode(el, { _$nodes: propView.build(), _$dlNodeType: 7 }, 0);
+}
diff --git a/packages/inula-v2/src/SnippetNode.js b/packages/inula-v2/src/SnippetNode.js
new file mode 100644
index 00000000..cb1522cf
--- /dev/null
+++ b/packages/inula-v2/src/SnippetNode.js
@@ -0,0 +1,18 @@
+import { DLNode, DLNodeType } from './DLNode';
+import { cached } from './store';
+
+export class SnippetNode extends DLNode {
+ constructor(depsArr) {
+ super(DLNodeType.Snippet);
+ this.depsArr = depsArr;
+ }
+
+ cached(deps, changed) {
+ if (!deps || !deps.length) return false;
+ const idx = Math.log2(changed);
+ const prevDeps = this.depsArr[idx];
+ if (cached(deps, prevDeps)) return true;
+ this.depsArr[idx] = deps;
+ return false;
+ }
+}
diff --git a/packages/inula-v2/src/TextNode.js b/packages/inula-v2/src/TextNode.js
new file mode 100644
index 00000000..5a55bee9
--- /dev/null
+++ b/packages/inula-v2/src/TextNode.js
@@ -0,0 +1,24 @@
+import { DLStore, cached } from './store';
+
+/**
+ * @brief Shorten document.createTextNode
+ * @param value
+ * @returns Text
+ */
+export function createTextNode(value, deps) {
+ const node = DLStore.document.createTextNode(value);
+ node.$$deps = deps;
+ return node;
+}
+
+/**
+ * @brief Update text node and check if the value is changed
+ * @param node
+ * @param value
+ */
+export function updateText(node, valueFunc, deps) {
+ if (cached(deps, node.$$deps)) return;
+ const value = valueFunc();
+ node.textContent = value;
+ node.$$deps = deps;
+}
diff --git a/packages/inula-v2/src/index.d.ts b/packages/inula-v2/src/index.d.ts
new file mode 100644
index 00000000..a3d3b4a0
--- /dev/null
+++ b/packages/inula-v2/src/index.d.ts
@@ -0,0 +1 @@
+export * from './types/index';
diff --git a/packages/inula-v2/src/index.js b/packages/inula-v2/src/index.js
new file mode 100644
index 00000000..449b9e63
--- /dev/null
+++ b/packages/inula-v2/src/index.js
@@ -0,0 +1,56 @@
+import { DLNode } from './DLNode';
+import { insertNode } from './HTMLNode';
+
+export * from './HTMLNode';
+export * from './CompNode';
+export * from './EnvNode';
+export * from './TextNode';
+export * from './PropView';
+export * from './SnippetNode';
+export * from './MutableNode/ForNode';
+export * from './MutableNode/ExpNode';
+export * from './MutableNode/CondNode';
+export * from './MutableNode/TryNode';
+
+import { DLStore } from './store';
+export { setGlobal, setDocument } from './store';
+
+function initStore() {
+ // Declare a global variable to store willUnmount functions
+ DLStore.global.WillUnmountStore = [];
+ // Declare a global variable to store didUnmount functions
+ DLStore.global.DidUnmountStore = [];
+}
+
+export function render(idOrEl, DL) {
+ let el = idOrEl;
+ if (typeof idOrEl === 'string') {
+ const elFound = DLStore.document.getElementById(idOrEl);
+ if (elFound) el = elFound;
+ else {
+ throw new Error(`DLight: Element with id ${idOrEl} not found`);
+ }
+ }
+ initStore();
+ el.innerHTML = '';
+ const dlNode = new DL();
+ dlNode._$init();
+ insertNode(el, dlNode, 0);
+ DLNode.runDidMount();
+}
+
+export function manual(callback, _deps) {
+ return callback();
+}
+export function escape(arg) {
+ return arg;
+}
+
+export const $ = escape;
+export const required = null;
+
+export function use() {
+ console.error(
+ 'DLight: use() is not supported be called directly. You can only assign `use(model)` to a dlight class property. Any other expressions are not allowed.'
+ );
+}
diff --git a/packages/inula-v2/src/store.js b/packages/inula-v2/src/store.js
new file mode 100644
index 00000000..76aead7b
--- /dev/null
+++ b/packages/inula-v2/src/store.js
@@ -0,0 +1,42 @@
+import { Store } from '@inula/store';
+
+// ---- Using external Store to store global and document
+// Because Store is a singleton, it is safe to use it as a global variable
+// If created in DLight package, different package versions will introduce
+// multiple Store instances.
+
+if (!('global' in Store)) {
+ if (typeof window !== 'undefined') {
+ Store.global = window;
+ } else if (typeof global !== 'undefined') {
+ Store.global = global;
+ } else {
+ Store.global = {};
+ }
+}
+if (!('document' in Store)) {
+ if (typeof document !== 'undefined') {
+ Store.document = document;
+ }
+}
+
+export const DLStore = { ...Store, delegatedEvents: new Set() };
+
+export function setGlobal(globalObj) {
+ DLStore.global = globalObj;
+}
+
+export function setDocument(customDocument) {
+ DLStore.document = customDocument;
+}
+
+/**
+ * @brief Compare the deps with the previous deps
+ * @param deps
+ * @param prevDeps
+ * @returns
+ */
+export function cached(deps, prevDeps) {
+ if (!prevDeps || deps.length !== prevDeps.length) return false;
+ return deps.every((dep, i) => !(dep instanceof Object) && prevDeps[i] === dep);
+}
diff --git a/packages/inula-v2/src/types/compTag.d.ts b/packages/inula-v2/src/types/compTag.d.ts
new file mode 100644
index 00000000..f2ea568c
--- /dev/null
+++ b/packages/inula-v2/src/types/compTag.d.ts
@@ -0,0 +1,74 @@
+import { type DLightHTMLAttributes } from './htmlTag';
+
+// a very magical solution
+// when vscode parse ts, if it is type A = B>, it will show the detailed type,
+// but if type A = B> & xxx, it will only show alias (here is A)
+// because I don't want to expose the detailed type, so type A = B> & Useless
+// but if type Useless = { useless: never } will cause this type to have an additional property userless
+// so just don't add key!
+type Useless = { [key in '']: never };
+
+export type DLightObject = {
+ [K in keyof T]-?: undefined extends T[K]
+ ? (value?: T[K]) => DLightObject>
+ : (value: T[K]) => DLightObject>;
+};
+interface CustomNodeProps {
+ willMount: (node: any) => void;
+ didMount: (node: any) => void;
+ willUnmount: (node: any) => void;
+ didUnmount: (node: any) => void;
+ didUpdate: (node: any, key: string, prevValue: any, currValue: any) => void;
+ ref: (node: any) => void;
+ elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
+ forwardProps: true | undefined;
+}
+
+export type ContentProp = T & { _$idContent: true };
+
+export type RemoveOptional = {
+ [K in keyof T]-?: T[K];
+};
+
+type IsAny = { _$isAny: true } extends T ? true : false;
+
+export type ContentKeyName = {
+ [K in keyof T]: IsAny extends true
+ ? never
+ : // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ T[K] extends ContentProp
+ ? K
+ : never;
+}[keyof T];
+
+export type CheckContent = RemoveOptional[ContentKeyName>];
+
+type CustomClassTag =
+ ContentKeyName> extends undefined
+ ? () => DLightObject
+ : undefined extends O[ContentKeyName>]
+ ? CheckContent extends ContentProp
+ ? (content?: U extends unknown ? any : unknown) => DLightObject>>>
+ : never
+ : CheckContent extends ContentProp
+ ? (content: U extends unknown ? any : unknown) => DLightObject>>>
+ : never;
+
+type CustomSnippetTag = T extends { content: infer U }
+ ? (content: U) => DLightObject>
+ : T extends { content?: infer U }
+ ? (content?: U) => DLightObject>
+ : () => DLightObject;
+
+type CustomTagType = CustomClassTag<
+ T & CustomNodeProps & (keyof G extends never ? object : DLightHTMLAttributes),
+ T
+> &
+ Useless;
+export type Typed = CustomTagType & Useless;
+export type SnippetTyped = CustomSnippetTag & Useless;
+
+export type Pretty = any;
+
+// ---- reverse
+export type UnTyped = T extends Typed ? U : never;
diff --git a/packages/inula-v2/src/types/envTag.d.ts b/packages/inula-v2/src/types/envTag.d.ts
new file mode 100644
index 00000000..c85e0030
--- /dev/null
+++ b/packages/inula-v2/src/types/envTag.d.ts
@@ -0,0 +1,6 @@
+// ---- env
+import { DLightObject } from './compTag';
+
+type AnyEnv = { _$anyEnv: true };
+
+export const env: () => T extends AnyEnv ? any : DLightObject;
diff --git a/packages/inula-v2/src/types/expressionTag.d.ts b/packages/inula-v2/src/types/expressionTag.d.ts
new file mode 100644
index 00000000..7546c296
--- /dev/null
+++ b/packages/inula-v2/src/types/expressionTag.d.ts
@@ -0,0 +1,13 @@
+interface ExpressionTag {
+ willMount: (node: any) => void;
+ didMount: (node: any) => void;
+ willUnmount: (node: any) => void;
+ didUnmount: (node: any) => void;
+ didUpdate: (node: any, key: string, prevValue: T, currValue: T) => void;
+ elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
+ ref: (node: any) => void;
+}
+
+type ExpressionTagFunc = (nodes: any) => ExpressionTag;
+
+export const _: ExpressionTagFunc;
diff --git a/packages/inula-v2/src/types/htmlTag/event.d.ts b/packages/inula-v2/src/types/htmlTag/event.d.ts
new file mode 100644
index 00000000..e6a3625e
--- /dev/null
+++ b/packages/inula-v2/src/types/htmlTag/event.d.ts
@@ -0,0 +1,516 @@
+export interface DLightGlobalEventHandlers {
+ /**
+ * Fires when the user aborts the download.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/abort_event)
+ */
+ onAbort: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationcancel_event) */
+ onAnimationCancel: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event) */
+ onAnimationEnd: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event) */
+ onAnimationIteration: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event) */
+ onAnimationStart: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/auxclick_event) */
+ onAuxClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/beforeinput_event) */
+ onBeforeInput: ((this: GlobalEventHandlers, ev: InputEvent) => any) | null;
+
+ /**
+ * Fires when the object loses the input focus.
+ * @param ev The focus event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/blur_event)
+ */
+ onBlur: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/cancel_event) */
+ onCancel: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs when playback is possible, but would require further buffering.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplay_event)
+ */
+ onCanPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplaythrough_event) */
+ onCanPlayThrough: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Fires when the contents of the object or selection have changed.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)
+ */
+ onChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Fires when the user clicks the left mouse button on the object
+ * @param ev The mouse event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/click_event)
+ */
+ onClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/close_event) */
+ onClose: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Fires when the user clicks the right mouse button in the client area, opening the context menu.
+ * @param ev The mouse event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/contextmenu_event)
+ */
+ onContextMenu: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/copy_event) */
+ onCopy: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLTrackElement/cuechange_event) */
+ onCueChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/cut_event) */
+ onCut: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
+
+ /**
+ * Fires when the user double-clicks the object.
+ * @param ev The mouse event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/dblclick_event)
+ */
+ onDblClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
+
+ /**
+ * Fires on the source object continuously during a drag operation.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drag_event)
+ */
+ onDrag: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
+
+ /**
+ * Fires on the source object when the user releases the mouse at the close of a drag operation.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragend_event)
+ */
+ onDragEnd: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
+
+ /**
+ * Fires on the target element when the user drags the object to a valid drop target.
+ * @param ev The drag event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragenter_event)
+ */
+ onDragEnter: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
+
+ /**
+ * Fires on the target object when the user moves the mouse out of a valid drop target during a drag operation.
+ * @param ev The drag event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragleave_event)
+ */
+ onDragLeave: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
+
+ /**
+ * Fires on the target element continuously while the user drags the object over a valid drop target.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragover_event)
+ */
+ onDragOver: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
+
+ /**
+ * Fires on the source object when the user starts to drag a text selection or selected object.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragstart_event)
+ */
+ onDragStart: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drop_event) */
+ onDrop: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
+
+ /**
+ * Occurs when the duration attribute is updated.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/durationchange_event)
+ */
+ onDurationChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs when the media element is reset to its initial state.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/emptied_event)
+ */
+ onEmptied: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs when the end of playback is reached.
+ * @param ev The event
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ended_event)
+ */
+ onEnded: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Fires when an error occurs during object loading.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/error_event)
+ */
+ onError: OnErrorEventHandler;
+
+ /**
+ * Fires when the object receives focus.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/focus_event)
+ */
+ onFocus: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/formdata_event) */
+ onFormData: ((this: GlobalEventHandlers, ev: FormDataEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/gotpointercapture_event) */
+ onGotPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event) */
+ onInput: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/invalid_event) */
+ onInvalid: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Fires when the user presses a key.
+ * @param ev The keyboard event
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keydown_event)
+ */
+ onKeyDown: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
+
+ /**
+ * Fires when the user presses an alphanumeric key.
+ * @param ev The event.
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keypress_event)
+ */
+ onKeyPress: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
+
+ /**
+ * Fires when the user releases a key.
+ * @param ev The keyboard event
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keyup_event)
+ */
+ onKeyUp: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
+
+ /**
+ * Fires immediately after the browser loads the object.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SVGElement/load_event)
+ */
+ onLoad: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs when media data is loaded at the current playback position.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadeddata_event)
+ */
+ onLoadedData: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs when the duration and dimensions of the media have been determined.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadedmetadata_event)
+ */
+ onLoadedMetadata: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs when Internet Explorer begins looking for media data.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadstart_event)
+ */
+ onLoadStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/lostpointercapture_event) */
+ onLostPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
+
+ /**
+ * Fires when the user clicks the object with either mouse button.
+ * @param ev The mouse event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousedown_event)
+ */
+ onMouseDown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseenter_event) */
+ onMouseEnter: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseleave_event) */
+ onMouseLeave: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
+
+ /**
+ * Fires when the user moves the mouse over the object.
+ * @param ev The mouse event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousemove_event)
+ */
+ onMouseMove: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
+
+ /**
+ * Fires when the user moves the mouse pointer outside the boundaries of the object.
+ * @param ev The mouse event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseout_event)
+ */
+ onMouseOut: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
+
+ /**
+ * Fires when the user moves the mouse pointer into the object.
+ * @param ev The mouse event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseover_event)
+ */
+ onMouseOver: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
+
+ /**
+ * Fires when the user releases a mouse button while the mouse is over the object.
+ * @param ev The mouse event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseup_event)
+ */
+ onMouseUp: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/paste_event) */
+ onPaste: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
+
+ /**
+ * Occurs when playback is paused.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/pause_event)
+ */
+ onPause: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs when the play method is requested.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/play_event)
+ */
+ onPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs when the audio or video has started playing.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/playing_event)
+ */
+ onPlaying: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointercancel_event) */
+ onPointerCancel: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerdown_event) */
+ onPointerDown: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerenter_event) */
+ onPointerEnter: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerleave_event) */
+ onPointerLeave: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointermove_event) */
+ onPointerMove: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerout_event) */
+ onPointerOut: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerover_event) */
+ onPointerOver: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerup_event) */
+ onPointerUp: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
+
+ /**
+ * Occurs to indicate progress while downloading media data.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/progress_event)
+ */
+ onProgress: ((this: GlobalEventHandlers, ev: ProgressEvent) => any) | null;
+
+ /**
+ * Occurs when the playback rate is increased or decreased.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ratechange_event)
+ */
+ onRateChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Fires when the user resets a form.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/reset_event)
+ */
+ onReset: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLVideoElement/resize_event) */
+ onResize: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
+
+ /**
+ * Fires when the user repositions the scroll box in the scroll bar on the object.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scroll_event)
+ */
+ onScroll: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scrollend_event) */
+ onScrollEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/securitypolicyviolation_event) */
+ onSecurityPolicyViolation: ((this: GlobalEventHandlers, ev: SecurityPolicyViolationEvent) => any) | null;
+
+ /**
+ * Occurs when the seek operation ends.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeked_event)
+ */
+ onSeeked: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs when the current playback position is moved.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeking_event)
+ */
+ onSeeking: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Fires when the current selection changes.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/select_event)
+ */
+ onSelect: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/selectionchange_event) */
+ onSelectionChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/selectstart_event) */
+ onSelectStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLSlotElement/slotchange_event) */
+ onSlotChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs when the download has stopped.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/stalled_event)
+ */
+ onStalled: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/submit_event) */
+ onSubmit: ((this: GlobalEventHandlers, ev: SubmitEvent) => any) | null;
+
+ /**
+ * Occurs if the load operation has been intentionally halted.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/suspend_event)
+ */
+ onSuspend: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs to indicate the current playback position.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/timeupdate_event)
+ */
+ onTimeUpdate: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDetailsElement/toggle_event) */
+ onToggle: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitioncancel_event) */
+ onTransitionCancel: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event) */
+ onTransitionEnd: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionrun_event) */
+ onTransitionRun: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionstart_event) */
+ onTransitionStart: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
+
+ /**
+ * Occurs when the volume is changed, or playback is muted or unmuted.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/volumechange_event)
+ */
+ onVolumeChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * Occurs when playback stops because the next frame of a video resource is not available.
+ * @param ev The event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/waiting_event)
+ */
+ onWaiting: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * @deprecated This is a legacy alias of `onAnimationEnd`.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event)
+ */
+ onWebkitAnimationEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * @deprecated This is a legacy alias of `onAnimationIteration`.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event)
+ */
+ onWebkitAnimationIteration: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * @deprecated This is a legacy alias of `onAnimationStart`.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event)
+ */
+ onWebkitAnimationStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /**
+ * @deprecated This is a legacy alias of `onTransitionEnd`.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event)
+ */
+ onWebkitTransitionEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
+
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/wheel_event) */
+ onWheel: ((this: GlobalEventHandlers, ev: WheelEvent) => any) | null;
+}
diff --git a/packages/inula-v2/src/types/htmlTag/htmlElement.d.ts b/packages/inula-v2/src/types/htmlTag/htmlElement.d.ts
new file mode 100644
index 00000000..80381a4c
--- /dev/null
+++ b/packages/inula-v2/src/types/htmlTag/htmlElement.d.ts
@@ -0,0 +1,33 @@
+import { type Properties } from 'csstype';
+
+// ---- Used to determine whether X and Y are equal, return A if equal, otherwise B
+type IfEquals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A : B;
+
+export type OmitIndexSignature = {
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ [KeyType in keyof ObjectType as {} extends Record ? never : KeyType]: ObjectType[KeyType];
+};
+
+// ---- For each key, check whether there is readonly, if there is, return never, and then Pick out is not never
+type WritableKeysOf = {
+ [P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never>;
+}[keyof T];
+type RemoveReadOnly = Pick>;
+
+// ---- Delete all functions
+type OmitFunction = Omit any ? K : never }[keyof T]>;
+
+type OmitFuncAndReadOnly = RemoveReadOnly>>;
+
+// ---- properties
+type OmitFuncAndReadOnlyProperty = Omit, 'className' | 'htmlFor' | 'style' | 'innerText'>;
+
+type CustomCSSProperties = {
+ [Key in `--${string}`]: string | number;
+};
+
+export type HTMLAttributes = OmitFuncAndReadOnlyProperty & {
+ style: Properties & CustomCSSProperties;
+ class: string;
+ for: string;
+};
diff --git a/packages/inula-v2/src/types/htmlTag/index.d.ts b/packages/inula-v2/src/types/htmlTag/index.d.ts
new file mode 100644
index 00000000..2f75cd64
--- /dev/null
+++ b/packages/inula-v2/src/types/htmlTag/index.d.ts
@@ -0,0 +1,236 @@
+import type { DLightGlobalEventHandlers } from './event';
+import type { OmitIndexSignature, HTMLAttributes } from './htmlElement';
+
+// ---- If there is an event(start with on), remove it
+export type PropertyWithEvent = Omit<
+ G,
+ {
+ [K in keyof G]: K extends `on${string}` ? K : never;
+ }[keyof G]
+> &
+ DLightGlobalEventHandlers;
+
+interface DLightHtmlProps {
+ ref: El | ((holder: El) => void) | undefined;
+ prop: Record;
+ attr: Record;
+ dataset: Record;
+ forwardProps: true | undefined;
+ willMount: (el: El) => void;
+ didMount: (el: El) => void;
+ willUnmount: (el: El) => void;
+ didUnmount: (el: El) => void;
+ didUpdate: (el: El, key: string, prevValue: T, currValue: T) => void;
+}
+
+export type DLightHTMLAttributes = DLightHtmlProps & HTMLAttributes & G;
+
+export type DLightHTMLAttributesFunc = {
+ [K in keyof DLightHTMLAttributes]: (
+ value?: DLightHTMLAttributes[K]
+ ) => Omit, K>;
+};
+
+export type DLightHtmlTagFunc = (
+ innerText?: string | number | ((View: never) => void)
+) => DLightHTMLAttributesFunc>, G, T>;
+
+export const a: DLightHtmlTagFunc;
+export const abbr: DLightHtmlTagFunc;
+export const address: DLightHtmlTagFunc;
+export const area: DLightHtmlTagFunc;
+export const article: DLightHtmlTagFunc;
+export const aside: DLightHtmlTagFunc;
+export const audio: DLightHtmlTagFunc;
+export const b: DLightHtmlTagFunc;
+export const base: DLightHtmlTagFunc;
+export const bdi: DLightHtmlTagFunc;
+export const bdo: DLightHtmlTagFunc;
+export const blockquote: DLightHtmlTagFunc;
+export const body: DLightHtmlTagFunc;
+export const br: DLightHtmlTagFunc;
+export const button: DLightHtmlTagFunc;
+export const canvas: DLightHtmlTagFunc;
+export const caption: DLightHtmlTagFunc;
+export const cite: DLightHtmlTagFunc;
+export const code: DLightHtmlTagFunc;
+export const col: DLightHtmlTagFunc;
+export const colgroup: DLightHtmlTagFunc;
+export const data: DLightHtmlTagFunc;
+export const datalist: DLightHtmlTagFunc;
+export const dd: DLightHtmlTagFunc;
+export const del: DLightHtmlTagFunc;
+export const details: DLightHtmlTagFunc;
+export const dfn: DLightHtmlTagFunc;
+export const dialog: DLightHtmlTagFunc;
+export const div: DLightHtmlTagFunc;
+export const dl: DLightHtmlTagFunc;
+export const dt: DLightHtmlTagFunc;
+export const em: DLightHtmlTagFunc;
+export const embed: DLightHtmlTagFunc;
+export const fieldset: DLightHtmlTagFunc;
+export const figcaption: DLightHtmlTagFunc;
+export const figure: DLightHtmlTagFunc;
+export const footer: DLightHtmlTagFunc;
+export const form: DLightHtmlTagFunc;
+export const h1: DLightHtmlTagFunc;
+export const h2: DLightHtmlTagFunc;
+export const h3: DLightHtmlTagFunc;
+export const h4: DLightHtmlTagFunc;
+export const h5: DLightHtmlTagFunc;
+export const h6: DLightHtmlTagFunc;
+export const head: DLightHtmlTagFunc;
+export const header: DLightHtmlTagFunc;
+export const hgroup: DLightHtmlTagFunc;
+export const hr: DLightHtmlTagFunc;
+export const html: DLightHtmlTagFunc;
+export const i: DLightHtmlTagFunc;
+export const iframe: DLightHtmlTagFunc;
+export const img: DLightHtmlTagFunc;
+export const input: DLightHtmlTagFunc;
+export const ins: DLightHtmlTagFunc;
+export const kbd: DLightHtmlTagFunc;
+export const label: DLightHtmlTagFunc;
+export const legend: DLightHtmlTagFunc;
+export const li: DLightHtmlTagFunc;
+export const link: DLightHtmlTagFunc;
+export const main: DLightHtmlTagFunc;
+export const map: DLightHtmlTagFunc;
+export const mark: DLightHtmlTagFunc;
+export const menu: DLightHtmlTagFunc;
+export const meta: DLightHtmlTagFunc;
+export const meter: DLightHtmlTagFunc;
+export const nav: DLightHtmlTagFunc;
+export const noscript: DLightHtmlTagFunc;
+export const object: DLightHtmlTagFunc;
+export const ol: DLightHtmlTagFunc;
+export const optgroup: DLightHtmlTagFunc;
+export const option: DLightHtmlTagFunc;
+export const output: DLightHtmlTagFunc;
+export const p: DLightHtmlTagFunc;
+export const picture: DLightHtmlTagFunc;
+export const pre: DLightHtmlTagFunc;
+export const progress: DLightHtmlTagFunc;
+export const q: DLightHtmlTagFunc;
+export const rp: DLightHtmlTagFunc;
+export const rt: DLightHtmlTagFunc;
+export const ruby: DLightHtmlTagFunc;
+export const s: DLightHtmlTagFunc;
+export const samp: DLightHtmlTagFunc;
+export const script: DLightHtmlTagFunc;
+export const section: DLightHtmlTagFunc;
+export const select: DLightHtmlTagFunc;
+export const slot: DLightHtmlTagFunc;
+export const small: DLightHtmlTagFunc;
+export const source: DLightHtmlTagFunc;
+export const span: DLightHtmlTagFunc;
+export const strong: DLightHtmlTagFunc;
+export const style: DLightHtmlTagFunc;
+export const sub: DLightHtmlTagFunc;
+export const summary: DLightHtmlTagFunc;
+export const sup: DLightHtmlTagFunc;
+export const table: DLightHtmlTagFunc;
+export const tbody: DLightHtmlTagFunc;
+export const td: DLightHtmlTagFunc;
+export const template: DLightHtmlTagFunc;
+export const textarea: DLightHtmlTagFunc;
+export const tfoot: DLightHtmlTagFunc;
+export const th: DLightHtmlTagFunc;
+export const thead: DLightHtmlTagFunc;
+export const time: DLightHtmlTagFunc;
+export const title: DLightHtmlTagFunc;
+export const tr: DLightHtmlTagFunc;
+export const track: DLightHtmlTagFunc;
+export const u: DLightHtmlTagFunc;
+export const ul: DLightHtmlTagFunc;
+export const var_: DLightHtmlTagFunc;
+export const video: DLightHtmlTagFunc;
+export const wbr: DLightHtmlTagFunc;
+export const acronym: DLightHtmlTagFunc;
+export const applet: DLightHtmlTagFunc;
+export const basefont: DLightHtmlTagFunc;
+export const bgsound: DLightHtmlTagFunc;
+export const big: DLightHtmlTagFunc;
+export const blink: DLightHtmlTagFunc;
+export const center: DLightHtmlTagFunc;
+export const dir: DLightHtmlTagFunc;
+export const font: DLightHtmlTagFunc;
+export const frame: DLightHtmlTagFunc;
+export const frameset: DLightHtmlTagFunc;
+export const isindex: DLightHtmlTagFunc;
+export const keygen: DLightHtmlTagFunc;
+export const listing: DLightHtmlTagFunc;
+export const marquee: DLightHtmlTagFunc;
+export const menuitem: DLightHtmlTagFunc;
+export const multicol: DLightHtmlTagFunc;
+export const nextid: DLightHtmlTagFunc;
+export const nobr: DLightHtmlTagFunc;
+export const noembed: DLightHtmlTagFunc;
+export const noframes: DLightHtmlTagFunc;
+export const param: DLightHtmlTagFunc;
+export const plaintext: DLightHtmlTagFunc;
+export const rb: DLightHtmlTagFunc;
+export const rtc: DLightHtmlTagFunc;
+export const spacer: DLightHtmlTagFunc;
+export const strike: DLightHtmlTagFunc;
+export const tt: DLightHtmlTagFunc;
+export const xmp: DLightHtmlTagFunc;
+export const animate: DLightHtmlTagFunc;
+export const animateMotion: DLightHtmlTagFunc;
+export const animateTransform: DLightHtmlTagFunc;
+export const circle: DLightHtmlTagFunc;
+export const clipPath: DLightHtmlTagFunc;
+export const defs: DLightHtmlTagFunc;
+export const desc: DLightHtmlTagFunc;
+export const ellipse: DLightHtmlTagFunc;
+export const feBlend: DLightHtmlTagFunc;
+export const feColorMatrix: DLightHtmlTagFunc;
+export const feComponentTransfer: DLightHtmlTagFunc;
+export const feComposite: DLightHtmlTagFunc;
+export const feConvolveMatrix: DLightHtmlTagFunc;
+export const feDiffuseLighting: DLightHtmlTagFunc;
+export const feDisplacementMap: DLightHtmlTagFunc;
+export const feDistantLight: DLightHtmlTagFunc;
+export const feDropShadow: DLightHtmlTagFunc;
+export const feFlood: DLightHtmlTagFunc;
+export const feFuncA: DLightHtmlTagFunc;
+export const feFuncB: DLightHtmlTagFunc;
+export const feFuncG: DLightHtmlTagFunc;
+export const feFuncR: DLightHtmlTagFunc;
+export const feGaussianBlur: DLightHtmlTagFunc;
+export const feImage: DLightHtmlTagFunc;
+export const feMerge: DLightHtmlTagFunc;
+export const feMergeNode: DLightHtmlTagFunc;
+export const feMorphology: DLightHtmlTagFunc;
+export const feOffset: DLightHtmlTagFunc;
+export const fePointLight: DLightHtmlTagFunc;
+export const feSpecularLighting: DLightHtmlTagFunc;
+export const feSpotLight: DLightHtmlTagFunc;
+export const feTile: DLightHtmlTagFunc;
+export const feTurbulence: DLightHtmlTagFunc;
+export const filter: DLightHtmlTagFunc;
+export const foreignObject: DLightHtmlTagFunc;
+export const g: DLightHtmlTagFunc;
+export const image: DLightHtmlTagFunc;
+export const line: DLightHtmlTagFunc;
+export const linearGradient: DLightHtmlTagFunc;
+export const marker: DLightHtmlTagFunc;
+export const mask: DLightHtmlTagFunc;
+export const metadata: DLightHtmlTagFunc;
+export const mpath: DLightHtmlTagFunc;
+export const path: DLightHtmlTagFunc;
+export const pattern: DLightHtmlTagFunc;
+export const polygon: DLightHtmlTagFunc;
+export const polyline: DLightHtmlTagFunc;
+export const radialGradient: DLightHtmlTagFunc;
+export const rect: DLightHtmlTagFunc;
+export const set: DLightHtmlTagFunc;
+export const stop: DLightHtmlTagFunc;
+export const svg: DLightHtmlTagFunc;
+export const switch_: DLightHtmlTagFunc;
+export const symbol: DLightHtmlTagFunc;
+export const text: DLightHtmlTagFunc;
+export const textPath: DLightHtmlTagFunc;
+export const tspan: DLightHtmlTagFunc;
+// export const use: DLightHtmlTagFunc
+export const view: DLightHtmlTagFunc;
diff --git a/packages/inula-v2/src/types/index.d.ts b/packages/inula-v2/src/types/index.d.ts
new file mode 100644
index 00000000..9bc1e12c
--- /dev/null
+++ b/packages/inula-v2/src/types/index.d.ts
@@ -0,0 +1,41 @@
+import { type Typed } from './compTag';
+import { type DLightHtmlTagFunc } from './htmlTag';
+export { type Properties as CSSProperties } from 'csstype';
+
+export const comp: (tag: T) => object extends T ? any : Typed;
+export const tag: (tag: any) => DLightHtmlTagFunc;
+
+export { _ } from './expressionTag';
+export * from './htmlTag';
+export * from './compTag';
+export * from './envTag';
+export * from './model';
+export const Static: any;
+export const Children: any;
+export const Content: any;
+export const Prop: any;
+export const Env: any;
+export const Watch: any;
+export const ForwardProps: any;
+export const Main: any;
+export const App: any;
+export const Mount: (idOrEl: string | HTMLElement) => any;
+
+// ---- With actual value
+export function render(idOrEl: string | HTMLElement, DL: any): void;
+export function manual(callback: () => T, _deps?: any[]): T;
+export function escape(arg: T): T;
+export function setGlobal(globalObj: any): void;
+export function setDocument(customDocument: any): void;
+export const $: typeof escape;
+export const View: any;
+export const Snippet: any;
+export const Model: any;
+export const update: any;
+export const required: any;
+export function insertChildren(parent: T, children: DLightViewProp): void;
+
+// ---- View types
+export type DLightViewComp = Typed;
+export type DLightViewProp = (View: any) => void;
+export type DLightViewLazy = () => Promise<{ default: T }>;
diff --git a/packages/inula-v2/src/types/model.d.ts b/packages/inula-v2/src/types/model.d.ts
new file mode 100644
index 00000000..ac4a4c0b
--- /dev/null
+++ b/packages/inula-v2/src/types/model.d.ts
@@ -0,0 +1,22 @@
+import { ContentKeyName, ContentProp } from './compTag';
+
+type RemoveDLightInternal = Omit;
+
+export type Modeling = (props: Props) => Model;
+
+type GetProps = keyof T extends never ? never : ContentKeyName extends undefined ? T : Omit>;
+
+type GetContent =
+ ContentKeyName extends undefined ? never : T[ContentKeyName] extends ContentProp ? U : never;
+
+export const use: (
+ model: M,
+ // @ts-expect-error Model should be a function
+ props?: GetProps[0]>,
+ // @ts-expect-error Model should be a function
+ content?: GetContent[0]>
+ // @ts-expect-error Model should be a function
+) => RemoveDLightInternal, Parameters[0]>;
+
+// @ts-expect-error Model should be a function
+export type ModelType = RemoveDLightInternal, Parameters[0]>;
diff --git a/packages/inula-v2/tsconfig.json b/packages/inula-v2/tsconfig.json
new file mode 100644
index 00000000..e0932d78
--- /dev/null
+++ b/packages/inula-v2/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "Node",
+ "strict": true,
+ "esModuleInterop": true
+ },
+ "ts-node": {
+ "esm": true
+ }
+}
\ No newline at end of file
diff --git a/packages/inula/scripts/rollup/rollup.config.js b/packages/inula/scripts/rollup/rollup.config.js
index 9228bb45..4e28e96d 100644
--- a/packages/inula/scripts/rollup/rollup.config.js
+++ b/packages/inula/scripts/rollup/rollup.config.js
@@ -109,13 +109,16 @@ function genConfig(mode) {
function genJSXRuntimeConfig(mode) {
return {
input: path.resolve(libDir, 'src', 'jsx-runtime.ts'),
- output: [{
- file: outputResolve('jsx-runtime.js'),
- format: 'cjs',
- }, {
- file: outputResolve('jsx-runtime.esm-browser.js'),
- format: 'esm',
- }],
+ output: [
+ {
+ file: outputResolve('jsx-runtime.js'),
+ format: 'cjs',
+ },
+ {
+ file: outputResolve('jsx-runtime.esm-browser.js'),
+ format: 'esm',
+ },
+ ],
plugins: [...getBasicPlugins(mode)],
};
}
diff --git a/packages/inula/src/inulax/types.ts b/packages/inula/src/inulax/types.ts
index 2814218c..044d7abb 100644
--- a/packages/inula/src/inulax/types.ts
+++ b/packages/inula/src/inulax/types.ts
@@ -31,7 +31,11 @@ export interface IObserver {
clearByVNode: (vNode: any) => void;
}
-export type StoreConfig, A extends UserActions, C extends UserComputedValues> = {
+export type StoreConfig<
+ S extends Record,
+ A extends UserActions