├── .github
└── workflows
│ ├── unis-babel-preset.yml
│ ├── unis-core.yml
│ ├── unis-dom.yml
│ ├── unis-router.yml
│ ├── unis-transition.yml
│ └── unis-vite-preset.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README-zh_CN.md
├── README.md
├── assets
├── bench.png
├── logo.svg
└── logo.txt
├── package.json
├── packages
├── unis-babel-preset
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ │ └── index.ts
│ ├── test
│ │ └── index.test.ts
│ └── tsconfig.json
├── unis-core
│ ├── .gitignore
│ ├── index.d.ts
│ ├── jsx-runtime
│ │ ├── jsx-dev-runtime.d.ts
│ │ ├── jsx-dev-runtime.js
│ │ ├── jsx-dev-runtime.mjs
│ │ ├── jsx-runtime.d.ts
│ │ ├── jsx-runtime.js
│ │ └── jsx-runtime.mjs
│ ├── package.json
│ ├── rollup.config.mjs
│ ├── src
│ │ ├── api
│ │ │ ├── use.ts
│ │ │ ├── useContext.ts
│ │ │ ├── useEffect.ts
│ │ │ ├── useId.ts
│ │ │ ├── useLayoutEffect.ts
│ │ │ ├── useMemo.ts
│ │ │ ├── useProps.ts
│ │ │ ├── useReducer.ts
│ │ │ ├── useRef.ts
│ │ │ ├── useState.ts
│ │ │ └── utils.ts
│ │ ├── commit.ts
│ │ ├── context.ts
│ │ ├── createTokTik.ts
│ │ ├── diff.ts
│ │ ├── fiber.ts
│ │ ├── h.ts
│ │ ├── index.ts
│ │ ├── reconcile.ts
│ │ ├── reconcileWalkHooks
│ │ │ ├── context.ts
│ │ │ ├── effect.ts
│ │ │ └── preElFiber.ts
│ │ ├── svg.ts
│ │ └── utils.ts
│ ├── test
│ │ └── utils.test.ts
│ ├── tsconfig.build.json
│ ├── tsconfig.json
│ ├── types
│ │ └── jsx.d.ts
│ └── vitest.config.ts
├── unis-dom
│ ├── .gitignore
│ ├── index.d.ts
│ ├── package.json
│ ├── rollup.config.mjs
│ ├── server.d.ts
│ ├── src
│ │ ├── browser
│ │ │ ├── __test__
│ │ │ │ ├── context.test.tsx
│ │ │ │ ├── dom.test.tsx
│ │ │ │ ├── effect.test.tsx
│ │ │ │ ├── hydrate.test.tsx
│ │ │ │ ├── memo.test.tsx
│ │ │ │ ├── portal.test.tsx
│ │ │ │ ├── reconcile.test.tsx
│ │ │ │ ├── util.ts
│ │ │ │ └── utils.test.ts
│ │ │ ├── const.ts
│ │ │ ├── index.ts
│ │ │ ├── operator.ts
│ │ │ ├── render.ts
│ │ │ └── toktik.ts
│ │ └── server
│ │ │ ├── __test__
│ │ │ └── server.test.tsx
│ │ │ ├── index.ts
│ │ │ └── operator.ts
│ ├── tsconfig.build.json
│ ├── tsconfig.json
│ └── vitest.config.ts
├── unis-example
│ ├── .gitignore
│ ├── index.html
│ ├── other.d.ts
│ ├── package.json
│ ├── postcss.config.js
│ ├── src
│ │ ├── Dialog.tsx
│ │ ├── Todo.tsx
│ │ ├── TodoItem
│ │ │ ├── index.module.css
│ │ │ └── index.tsx
│ │ ├── Welcome
│ │ │ ├── index.module.css
│ │ │ └── index.tsx
│ │ ├── bg.jpeg
│ │ ├── global.css
│ │ ├── hooks
│ │ │ └── update.ts
│ │ ├── index.module.css
│ │ └── index.tsx
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ └── vite.config.js
├── unis-router
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.mjs
│ ├── src
│ │ ├── components
│ │ │ ├── BrowserRouter.tsx
│ │ │ ├── Link.tsx
│ │ │ ├── NavLink.tsx
│ │ │ ├── Outlet.tsx
│ │ │ ├── Redirect.tsx
│ │ │ ├── Route.tsx
│ │ │ └── Routes.tsx
│ │ ├── context.tsx
│ │ ├── hooks
│ │ │ ├── uHistory.ts
│ │ │ ├── uLocation.ts
│ │ │ ├── uParams.ts
│ │ │ ├── uRouter.tsx
│ │ │ └── uTargetPath.tsx
│ │ ├── index.ts
│ │ ├── types.ts
│ │ ├── utils.test.tsx
│ │ └── utils.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── unis-transition
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.mjs
│ ├── src
│ │ ├── CSSTransition.ts
│ │ ├── TransitionGroup.ts
│ │ ├── hooks
│ │ │ ├── uInstance.ts
│ │ │ ├── uTransition.ts
│ │ │ ├── uUpdate.ts
│ │ │ └── uWatch.ts
│ │ └── index.ts
│ └── tsconfig.json
└── unis-vite-preset
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ └── index.ts
│ └── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.json
/.github/workflows/unis-babel-preset.yml:
--------------------------------------------------------------------------------
1 | name: "@unis/babel-preset CI/CD"
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | paths:
8 | - 'packages/unis-babel-preset/**'
9 |
10 | jobs:
11 |
12 | "test":
13 | runs-on: ubuntu-latest
14 | if: ${{contains(github.event.head_commit.message, '(babel-preset):')}}
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: actions/setup-node@v2
18 | with:
19 | node-version: 16.x
20 | registry-url: "https://registry.npmjs.org"
21 | - uses: pnpm/action-setup@v2.0.1
22 | with:
23 | version: 7.9.0
24 | - run: |
25 | pnpm install
26 | cd packages/unis-core
27 | pnpm build
28 | cd ../unis-babel-preset
29 | pnpm test
30 |
31 | "publish":
32 | needs: test
33 | runs-on: ubuntu-latest
34 | if: ${{startsWith(github.event.head_commit.message, 'release(babel-preset):')}}
35 | steps:
36 | - uses: actions/checkout@v2
37 | - uses: actions/setup-node@v2
38 | with:
39 | node-version: 16.x
40 | registry-url: "https://registry.npmjs.org"
41 | - uses: pnpm/action-setup@v2.0.1
42 | with:
43 | version: 7.9.0
44 | - run: |
45 | pnpm install
46 | cd packages/unis-core
47 | pnpm build
48 | cd ../unis-babel-preset
49 | pnpm build
50 | pnpm publish --no-git-checks --access public
51 | env:
52 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
53 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
54 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/unis-core.yml:
--------------------------------------------------------------------------------
1 | name: "@unis/core CI/CD"
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | paths:
8 | - 'packages/unis-core/**'
9 |
10 | jobs:
11 |
12 | "test":
13 | runs-on: ubuntu-latest
14 | if: ${{contains(github.event.head_commit.message, '(core):')}}
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: actions/setup-node@v2
18 | with:
19 | node-version: 16.x
20 | registry-url: "https://registry.npmjs.org"
21 | - uses: pnpm/action-setup@v2.0.1
22 | with:
23 | version: 7.9.0
24 | - run: |
25 | pnpm install
26 | cd packages/unis-core
27 | pnpm test
28 |
29 | "publish":
30 | needs: test
31 | runs-on: ubuntu-latest
32 | if: ${{startsWith(github.event.head_commit.message, 'release(core):')}}
33 | steps:
34 | - uses: actions/checkout@v2
35 | - uses: actions/setup-node@v2
36 | with:
37 | node-version: 16.x
38 | registry-url: "https://registry.npmjs.org"
39 | - uses: pnpm/action-setup@v2.0.1
40 | with:
41 | version: 7.1.2
42 | - run: |
43 | pnpm install
44 | cd packages/unis-core
45 | pnpm build
46 | pnpm publish --no-git-checks --access public
47 | env:
48 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
49 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
50 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/unis-dom.yml:
--------------------------------------------------------------------------------
1 | name: "@unis/dom CI/CD"
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | paths:
8 | - 'packages/unis-dom/**'
9 |
10 | jobs:
11 |
12 | "test":
13 | runs-on: ubuntu-latest
14 | if: ${{contains(github.event.head_commit.message, '(dom):')}}
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: actions/setup-node@v2
18 | with:
19 | node-version: 16.x
20 | registry-url: "https://registry.npmjs.org"
21 | - uses: pnpm/action-setup@v2.0.1
22 | with:
23 | version: 7.9.0
24 | - run: |
25 | pnpm install
26 | cd packages/unis-core
27 | pnpm build
28 | cd ../unis-vite-preset
29 | pnpm build
30 | cd ../unis-dom
31 | pnpm test
32 |
33 | "publish":
34 | needs: test
35 | runs-on: ubuntu-latest
36 | if: ${{startsWith(github.event.head_commit.message, 'release(dom):')}}
37 | steps:
38 | - uses: actions/checkout@v2
39 | - uses: actions/setup-node@v2
40 | with:
41 | node-version: 16.x
42 | registry-url: "https://registry.npmjs.org"
43 | - uses: pnpm/action-setup@v2.0.1
44 | with:
45 | version: 7.1.2
46 | - run: |
47 | pnpm install
48 | cd packages/unis-core
49 | pnpm build
50 | cd ../unis-dom
51 | pnpm build
52 | pnpm publish --no-git-checks --access public
53 | env:
54 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
55 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/unis-router.yml:
--------------------------------------------------------------------------------
1 | name: "@unis/router CI/CD"
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | paths:
8 | - 'packages/unis-router/**'
9 |
10 | jobs:
11 | "test":
12 | runs-on: ubuntu-latest
13 | if: ${{contains(github.event.head_commit.message, '(router):')}}
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: actions/setup-node@v2
17 | with:
18 | node-version: 16.x
19 | registry-url: "https://registry.npmjs.org"
20 | - uses: pnpm/action-setup@v2.0.1
21 | with:
22 | version: 7.9.0
23 | - run: |
24 | pnpm install
25 | cd packages/unis-core
26 | pnpm build
27 | cd ../unis-vite-preset
28 | pnpm build
29 | cd ../unis-router
30 | pnpm test
31 |
32 | "publish":
33 | needs: test
34 | runs-on: ubuntu-latest
35 | if: ${{startsWith(github.event.head_commit.message, 'release(router):')}}
36 | steps:
37 | - uses: actions/checkout@v2
38 | - uses: actions/setup-node@v2
39 | with:
40 | node-version: 16.x
41 | registry-url: "https://registry.npmjs.org"
42 | - uses: pnpm/action-setup@v2.0.1
43 | with:
44 | version: 7.9.0
45 | - run: |
46 | pnpm install
47 | cd packages/unis-core
48 | pnpm build
49 | cd ../unis-router
50 | pnpm build
51 | pnpm publish --no-git-checks --access public
52 | env:
53 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
54 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
55 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/unis-transition.yml:
--------------------------------------------------------------------------------
1 | name: "@unis/transition CI/CD"
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | paths:
8 | - 'packages/unis-transition/**'
9 |
10 | jobs:
11 |
12 | "publish":
13 | runs-on: ubuntu-latest
14 | if: ${{startsWith(github.event.head_commit.message, 'release(transition):')}}
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: actions/setup-node@v2
18 | with:
19 | node-version: 16.x
20 | registry-url: "https://registry.npmjs.org"
21 | - uses: pnpm/action-setup@v2.0.1
22 | with:
23 | version: 7.9.0
24 | - run: |
25 | pnpm install
26 | cd packages/unis-core
27 | pnpm build
28 | cd ../unis-transition
29 | pnpm build
30 | pnpm publish --no-git-checks --access public
31 | env:
32 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
33 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/unis-vite-preset.yml:
--------------------------------------------------------------------------------
1 | name: "@unis/vite-preset CI/CD"
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | paths:
8 | - 'packages/unis-vite-preset/**'
9 |
10 | jobs:
11 |
12 | "publish":
13 | runs-on: ubuntu-latest
14 | if: ${{startsWith(github.event.head_commit.message, 'release(vite-preset):')}}
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: actions/setup-node@v2
18 | with:
19 | node-version: 16.x
20 | registry-url: "https://registry.npmjs.org"
21 | - uses: pnpm/action-setup@v2.0.1
22 | with:
23 | version: 7.9.0
24 | - run: |
25 | pnpm install
26 | cd packages/unis-core
27 | pnpm build
28 | cd ../unis-vite-preset
29 | pnpm build
30 | pnpm publish --no-git-checks --access public
31 | env:
32 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
33 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependency directories
2 | node_modules/
3 |
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "[typescriptreact]": {
4 | "editor.formatOnSave": true
5 | },
6 | "[typescript]": {
7 | "editor.formatOnSave": true
8 | },
9 | "[javascript]": {
10 | "editor.formatOnSave": true
11 | },
12 | "[markdown]": {
13 | "editor.formatOnSave": true
14 | },
15 | "editor.tabSize": 2,
16 | "editor.insertSpaces": true,
17 | "files.associations": {
18 | "*.json": "jsonc"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021-present anuoua
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README-zh_CN.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [](https://github.com/anuoua/unis/actions/workflows/unis-core.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-dom.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-router.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-transition.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-vite-preset.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-babel-preset.yml)
6 |
7 | # Unis
8 |
9 | Unis 是一款新的前端框架,创新的编译策略打造的组件 API 帮助你更加轻松的创建网页 UI。
10 |
11 | ## 性能
12 |
13 |
14 |
15 | ## 安装
16 |
17 | ```bash
18 | npm i @unis/core @unis/dom
19 | ```
20 |
21 | ## Vite 开发
22 |
23 | ```shell
24 | npm i vite @unis/vite-preset -D
25 | ```
26 |
27 | vite.config.js
28 |
29 | ```javascript
30 | import { defineConfig } from "vite";
31 | import { unisPreset } from "@unis/vite-preset";
32 |
33 | export default defineConfig({
34 | plugins: [unisPreset()],
35 | });
36 | ```
37 |
38 | tsconfig.json
39 |
40 | ```json
41 | {
42 | "compilerOptions": {
43 | "jsx": "react-jsx",
44 | "jsxImportSource": "@unis/core"
45 | }
46 | }
47 | ```
48 |
49 | index.html
50 |
51 | ```javascript
52 |
53 | ...
54 |
55 |
56 |
57 |
58 |
59 | ```
60 |
61 | index.tsx
62 |
63 | ```javascript
64 | function App() {
65 | return () => hello
;
66 | }
67 |
68 | render( , document.querySelector("#root"));
69 | ```
70 |
71 | ## 用法
72 |
73 | Unis 并不是 React 的复刻,而是保留了 React 使用体验的全新框架,unis 的用法很简单,熟悉 React 的可以很快上手。
74 |
75 | ### 组件
76 |
77 | 在 unis 中组件是一个高阶函数。
78 |
79 | ```javascript
80 | import { render } from "@unis/dom";
81 |
82 | const App = () => {
83 | return () => (
84 | // 返回一个函数
85 | hello world
86 | );
87 | };
88 |
89 | render( , document.querySelector("#root"));
90 | ```
91 |
92 | ### 组件状态
93 |
94 | Unis 中的 `useState` 用法和 React 相似,但是要注意的是 unis 中 `use` 系列方法,定义类型必须为 `let` ,因为 unis 使用了 Callback Reassign 编译策略,[@callback-reassign/rollup-plugin](https://github.com/anuoua/callback-reassign) 帮我们补全了 Callback Reassign 代码。
95 |
96 | ```javascript
97 | import { useState } from "@unis/core";
98 |
99 | const App = () => {
100 | let [msg, setMsg] = useState("hello");
101 | /**
102 | * Compile to:
103 | *
104 | * let [msg, setMsg] = useState('hello', ([$0, $1]) => { msg = $0; setMsg = $1 });
105 | */
106 | return () => {msg}
;
107 | };
108 | ```
109 |
110 | ### Props
111 |
112 | Unis 直接使用 props 会无法获取最新值,所以 unis 提供了 useProps。
113 |
114 | ```javascript
115 | import { useProps } from "@unis/core";
116 |
117 | const App = (p) => {
118 | let { some } = useProps(p);
119 | /**
120 | * Compile to:
121 | *
122 | * let { some } = useProps(p, ({ some: $0 }) => { some = $0 });
123 | */
124 | return () => {some}
;
125 | };
126 | ```
127 |
128 | ### 副作用
129 |
130 | Unis 保留了和 React 基本一致的 `useEffect` 和 `useLayoutEffect` ,但 deps 是一个返回数组的函数。
131 |
132 | ```javascript
133 | import { useEffect } from "@unis/core";
134 |
135 | const App = () => {
136 | useEffect(
137 | () => {
138 | // ...
139 | return () => {
140 | // 清理...
141 | };
142 | },
143 | () => [] // deps 是一个返回数组的函数
144 | );
145 |
146 | return () => hello
;
147 | };
148 | ```
149 |
150 | ### 自定义 hook
151 |
152 | Unis 的自定义 hook ,在有返回值的场景需要搭配 `use` 方法使用,原因则是前面提到的 Callback Reassign 编译策略。自定义 hook 的命名我们约定以小写字母 `u` 开头,目的是用于区分其他函数,同时在 IDE 的提示下更加方便的导入。
153 |
154 | ```javascript
155 | import { use, useState } from "@unis/core";
156 |
157 | // 创建自定义 hook 高阶函数
158 | const uCount = () => {
159 | let [count, setCount] = useState(0);
160 | const add = () => setCount(count + 1);
161 | return () => [count, add];
162 | };
163 |
164 | // 通过 `use` 使用 hook
165 | function App() {
166 | let [count, add] = use(uCount());
167 | /**
168 | * Compile to:
169 | *
170 | * let [count, add] = use(uCount(), ([$0, $1]) => { count = $0; add = $1 });
171 | */
172 | return () => {count}
;
173 | }
174 | ```
175 |
176 | ## 特性
177 |
178 | ### Fragment
179 |
180 | ```javascript
181 | import { Fragment } from "@unis/core";
182 |
183 | function App() {
184 | return () => (
185 |
186 |
187 |
188 |
189 | );
190 | }
191 | ```
192 |
193 | ### Portal
194 |
195 | ```javascript
196 | import { createPortal } from "@unis/core";
197 |
198 | function App() {
199 | return () => createPortal(
, document.body);
200 | }
201 | ```
202 |
203 | ### Context
204 |
205 | ```javascript
206 | import { createContext } from "@unis/core";
207 | import { render } from "@unis/dom";
208 |
209 | const ThemeContext = createContext("light");
210 |
211 | function App() {
212 | let theme = useContext(ThemeContext);
213 |
214 | return () => {theme}
;
215 | }
216 |
217 | render(
218 |
219 |
220 | ,
221 | document.querySelector("#root")
222 | );
223 | ```
224 |
225 | ## SSR 服务端渲染
226 |
227 | 服务端
228 |
229 | ```javascript
230 | import express from "express";
231 | import { renderToString } from "@unis/dom/server";
232 |
233 | const app = express();
234 |
235 | app.get("/", (req, res) => {
236 | const SSR_CONTENT = renderToString(hello world
);
237 |
238 | res.send(`
239 |
240 |
241 |
242 | ${SSR_CONTENT}
243 |
244 |
245 | `);
246 | });
247 | ```
248 |
249 | 客户端
250 |
251 | ```javascript
252 | import { render } from "@unis/dom";
253 |
254 | render(
255 | ,
256 | document.querySelector("#root"),
257 | true // true 代表使用 hydrate (水合)进行渲染,复用 server 端的内容。
258 | );
259 | ```
260 |
261 | ## Todo 项目
262 |
263 | 完整项目请查看
264 |
265 | - [packages/unis-example](packages/unis-example) Todo 示例
266 | - [stackbliz](https://stackblitz.com/edit/vitejs-vite-8hn3pz) 试用
267 |
268 | ## API
269 |
270 | - Core
271 |
272 | - h
273 | - h2 (for jsx2)
274 | - Fragment
275 | - createPortal
276 | - createContext
277 | - render
278 | - memo
279 |
280 | - Hooks
281 | - use
282 | - useProps
283 | - useState
284 | - useReducer
285 | - useContext
286 | - useMemo
287 | - useEffect
288 | - useRef
289 | - useId
290 |
291 | ## License
292 |
293 | MIT @anuoua
294 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [](https://github.com/anuoua/unis/actions/workflows/unis-core.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-dom.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-router.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-transition.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-vite-preset.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-babel-preset.yml)
6 |
7 | # Unis [中文](./README-zh_CN.md)
8 |
9 | Unis is a new front-end framework. Its innovative compilation strategy and component API built help you create web UI more easily.
10 |
11 | ## Performance
12 |
13 |
14 |
15 | ## Installation
16 |
17 | ```bash
18 | npm i @unis/core @unis/dom
19 | ```
20 |
21 | ## Vite Development
22 |
23 | ```shell
24 | npm i vite @unis/vite-preset -D
25 | ```
26 |
27 | vite.config.js
28 |
29 | ```javascript
30 | import { defineConfig } from "vite";
31 | import { unisPreset } from "@unis/vite-preset";
32 |
33 | export default defineConfig({
34 | plugins: [unisPreset()],
35 | });
36 | ```
37 |
38 | tsconfig.json
39 |
40 | ```json
41 | {
42 | "compilerOptions": {
43 | "jsx": "react-jsx",
44 | "jsxImportSource": "@unis/core"
45 | }
46 | }
47 | ```
48 |
49 | index.html
50 |
51 | ```javascript
52 |
53 | ...
54 |
55 |
56 |
57 |
58 |
59 | ```
60 |
61 | index.tsx
62 |
63 | ```javascript
64 | function App() {
65 | return () => hello
;
66 | }
67 |
68 | render( , document.querySelector("#root"));
69 | ```
70 |
71 | ## Usage
72 |
73 | Unis is not a replica of React, but a brand new framework that retains the user experience of React. Unis is easy to use, and those who are familiar with React can quickly get started.
74 |
75 | ### Components
76 |
77 | In Unis, the component is a higher-order function.
78 |
79 | ```javascript
80 | import { render } from "@unis/dom";
81 |
82 | const App = () => {
83 | return () => (
84 | // Returns a function
85 | hello world
86 | );
87 | };
88 |
89 | render( , document.querySelector("#root"));
90 | ```
91 |
92 | ### Component State
93 |
94 | The usage of `useState` in Unis is similar to React, but it should be noted that for the `use` method series in Unis, the defined type must be `let`. This is because Unis uses the Callback Reassign compilation strategy, and [@callback-reassign/rollup-plugin](https://github.com/anuoua/callback-reassign) helps us complete the Callback Reassign code.
95 |
96 | ```javascript
97 | import { useState } from "@unis/core";
98 |
99 | const App = () => {
100 | let [msg, setMsg] = useState("hello");
101 | /**
102 | * Compile to:
103 | *
104 | * let [msg, setMsg] = useState('hello', ([$0, $1]) => { msg = $0; setMsg = $1 });
105 | */
106 | return () => {msg}
;
107 | };
108 | ```
109 |
110 | ### Props
111 |
112 | Directly using `props` in Unis will be unable to get the latest value, so Unis provides `useProps`.
113 |
114 | ```javascript
115 | import { useProps } from "@unis/core";
116 |
117 | const App = (p) => {
118 | let { some } = useProps(p);
119 | /**
120 | * Compile to:
121 | *
122 | * let { some } = useProps(p, ({ some: $0 }) => { some = $0 });
123 | */
124 | return () => {some}
;
125 | };
126 | ```
127 |
128 | ### Side Effects
129 |
130 | Unis retains the familiar `useEffect` and `useLayoutEffect` methods from React, but the `deps` parameter is a function that returns an array.
131 |
132 | ```javascript
133 | import { useEffect } from "@unis/core";
134 |
135 | const App = () => {
136 | useEffect(
137 | () => {
138 | // ...
139 | return () => {
140 | // Clean up...
141 | };
142 | },
143 | () => [] // deps is a function that returns an array
144 | );
145 |
146 | return () => hello
;
147 | };
148 | ```
149 |
150 | ### Custom Hook
151 |
152 | For Unis' custom hooks that have a return value, the `use` method should be used accordingly, due to the Callback Reassign compilation strategy mentioned earlier. We conventionally name custom hooks with a lowercase `u` at the beginning, to differentiate them from other functions and make them easy to import with IDE hints.
153 |
154 | ```javascript
155 | import { use, useState } from "@unis/core";
156 |
157 | // Create a higher-order function for the custom hook
158 | const uCount = () => {
159 | let [count, setCount] = useState(0);
160 | const add = () => setCount(count + 1);
161 | return () => [count, add];
162 | };
163 |
164 | // Use the hook through `use`
165 | function App() {
166 | let [count, add] = use(uCount());
167 | /**
168 | * Compile to:
169 | *
170 | * let [count, add] = use(uCount(), ([$0, $1]) => { count = $0; add = $1 });
171 | */
172 | return () => {count}
;
173 | }
174 | ```
175 |
176 | ## Features
177 |
178 | ### Fragment
179 |
180 | ```javascript
181 | import { Fragment } from "@unis/core";
182 |
183 | function App() {
184 | return () => (
185 |
186 |
187 |
188 |
189 | );
190 | }
191 | ```
192 |
193 | ### Portal
194 |
195 | ```javascript
196 | import { createPortal } from "@unis/core";
197 |
198 | function App() {
199 | return () => createPortal(
, document.body);
200 | }
201 | ```
202 |
203 | ### Context
204 |
205 | ```javascript
206 | import { createContext } from "@unis/core";
207 | import { render } from "@unis/dom";
208 |
209 | const ThemeContext = createContext("light");
210 |
211 | function App() {
212 | let theme = useContext(ThemeContext);
213 |
214 | return () => {theme}
;
215 | }
216 |
217 | render(
218 |
219 |
220 | ,
221 | document.querySelector("#root")
222 | );
223 | ```
224 |
225 | ## Server-Side Rendering
226 |
227 | Server
228 |
229 | ```javascript
230 | import express from "express";
231 | import { renderToString } from "@unis/dom/server";
232 |
233 | const app = express();
234 |
235 | app.get("/", (req, res) => {
236 | const SSR_CONTENT = renderToString(hello world
);
237 |
238 | res.send(`
239 |
240 |
241 |
242 | ${SSR_CONTENT}
243 |
244 |
245 | `);
246 | });
247 | ```
248 |
249 | Client
250 |
251 | ```javascript
252 | import { render } from "@unis/dom";
253 |
254 | render(
255 | ,
256 | document.querySelector("#root"),
257 | true // true means using hydration to render and reuse the server-side rendered content.
258 | );
259 | ```
260 |
261 | ## Todo Project
262 |
263 | See complete project at
264 |
265 | - [packages/unis-example](packages/unis-example) Todo example
266 | - [stackbliz](https://stackblitz.com/edit/vitejs-vite-8hn3pz) Try it out
267 |
268 | ## API
269 |
270 | - Core
271 |
272 | - h
273 | - h2 (for jsx2)
274 | - Fragment
275 | - createPortal
276 | - createContext
277 | - render
278 | - memo
279 |
280 | - Hooks
281 | - use
282 | - useProps
283 | - useState
284 | - useReducer
285 | - useContext
286 | - useMemo
287 | - useEffect
288 | - useRef
289 | - useId
290 |
291 | ## License
292 |
293 | MIT @anuoua
294 |
--------------------------------------------------------------------------------
/assets/bench.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anuoua/unis/a40e220ce9e68377e83a8beb7b7f54782bf3e8ea/assets/bench.png
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/assets/logo.txt:
--------------------------------------------------------------------------------
1 |
2 | ████
3 | ██ █
4 | █ █
5 | ███ █ █
6 | █ ████ ██████
7 | █ ██ ██ ██
8 | ██ ███
9 | █ █ █ ██
10 | ██ ███
11 | █ █
12 | ██ ████
13 | ██████████
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "unis",
3 | "version": "0.0.0",
4 | "description": "",
5 | "private": true,
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "workspaces": {
10 | "packages": [
11 | "./packages/*"
12 | ]
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/anuoua/unis.git"
17 | },
18 | "author": "anuoua",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/anuoua/unis/issues"
22 | },
23 | "homepage": "https://github.com/anuoua/unis#readme"
24 | }
25 |
--------------------------------------------------------------------------------
/packages/unis-babel-preset/.gitignore:
--------------------------------------------------------------------------------
1 | build/
--------------------------------------------------------------------------------
/packages/unis-babel-preset/README.md:
--------------------------------------------------------------------------------
1 | # Unis Babel Preset
2 |
3 | Unis develop preset for babel.
4 |
5 | ## Install
6 |
7 | ```shell
8 | npm add -D @unis/babel-preset
9 | ```
10 |
11 | ## Usage
12 |
13 | .babelrc.json or babel.config.js
14 |
15 | ```javascript
16 | {
17 | "presets": ["@unis/babel-preset"]
18 | }
19 | ```
20 |
21 | If you use @babel/preset-env, please use relatively new targets. There is a bug in the babel transformation of destructuring syntax. e.g.
22 |
23 | ```javascript
24 | {
25 | "presets": [
26 | [
27 | "@babel/preset-env",
28 | {
29 | targets: "> 0.25%, not dead",
30 | }
31 | ],
32 | "@unis/babel-preset"
33 | ]
34 | }
35 | ```
36 |
--------------------------------------------------------------------------------
/packages/unis-babel-preset/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@unis/babel-preset",
3 | "version": "0.0.2",
4 | "description": "Unis babel preset",
5 | "main": "build/index.js",
6 | "module": "build/index.mjs",
7 | "types": "build/index.d.ts",
8 | "typings": "build/index.d.ts",
9 | "scripts": {
10 | "build": "rimraf build && rollup --config && tsc",
11 | "build:dev": "cross-env NODE_ENV=development pnpm build",
12 | "test": "vitest run"
13 | },
14 | "exports": {
15 | ".": {
16 | "require": "./build/index.js",
17 | "import": "./build/index.mjs"
18 | }
19 | },
20 | "keywords": [
21 | "babel",
22 | "preset",
23 | "unis"
24 | ],
25 | "files": [
26 | "build"
27 | ],
28 | "author": "anuoua",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/anuoua/unis/issues"
32 | },
33 | "homepage": "https://github.com/anuoua/unis/tree/main/packages/unis-babel-preset",
34 | "peerDependencies": {
35 | "@unis/core": "workspace:^"
36 | },
37 | "dependencies": {
38 | "@babel/plugin-syntax-jsx": "^7.21.4",
39 | "@babel/plugin-transform-react-jsx": "^7.21.0",
40 | "@callback-reassign/babel-plugin": "^0.0.1"
41 | },
42 | "devDependencies": {
43 | "@babel/core": "^7.21.4",
44 | "@rollup/plugin-node-resolve": "^13.0.6",
45 | "@types/babel__core": "^7.20.0",
46 | "@unis/core": "workspace:^",
47 | "cross-env": "^7.0.3",
48 | "esbuild": "^0.13.13",
49 | "rimraf": "^3.0.2",
50 | "rollup": "^2.72.0",
51 | "rollup-plugin-esbuild": "^4.6.0",
52 | "typescript": "^4.4.4",
53 | "vite": "^4.2.1",
54 | "vitest": "^0.29.8"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/packages/unis-babel-preset/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "rollup";
2 | import { nodeResolve } from "@rollup/plugin-node-resolve";
3 | import esbuild from "rollup-plugin-esbuild";
4 |
5 | const configGen = (format) =>
6 | defineConfig({
7 | input: "src/index.ts",
8 | external: [
9 | /^@unis/,
10 | "@callback-reassign/rollup-plugin",
11 | "@babel/plugin-syntax-jsx",
12 | "@babel/plugin-transform-react-jsx",
13 | ],
14 | output: [
15 | {
16 | dir: "build",
17 | entryFileNames: `index.${format === "esm" ? "mjs" : "js"}`,
18 | format,
19 | sourcemap: true,
20 | },
21 | ],
22 | plugins: [
23 | nodeResolve({
24 | modulesOnly: true,
25 | }),
26 | esbuild({
27 | sourceMap: true,
28 | minify: process.env.NODE_ENV === "development" ? false : true,
29 | target: "esnext",
30 | }),
31 | ],
32 | });
33 |
34 | const config = [configGen("cjs"), configGen("esm")];
35 |
36 | export default config;
37 |
--------------------------------------------------------------------------------
/packages/unis-babel-preset/src/index.ts:
--------------------------------------------------------------------------------
1 | import { unisFns } from "@unis/core";
2 | import reassign from "@callback-reassign/babel-plugin";
3 | // @ts-ignore
4 | import syntaxJsx from "@babel/plugin-syntax-jsx";
5 | // @ts-ignore
6 | import transformReactJsx from "@babel/plugin-transform-react-jsx";
7 |
8 | export default function unisPreset() {
9 | return {
10 | plugins: [
11 | syntaxJsx,
12 | [
13 | transformReactJsx,
14 | {
15 | runtime: "automatic",
16 | importSource: "@unis/core",
17 | },
18 | ],
19 | [
20 | reassign,
21 | {
22 | targetFns: {
23 | "@unis/core": unisFns,
24 | },
25 | },
26 | ],
27 | ],
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/packages/unis-babel-preset/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import { it, expect } from "vitest";
2 | import { transform } from "@babel/core";
3 | import unisPreset from "../src/index";
4 |
5 | const code = `
6 | import { useState } from "@unis/core";
7 | let [a, seta] = useState(1);
8 | `;
9 |
10 | const transformed = `import { useState } from "@unis/core";
11 | let [a, seta] = useState(1, ([$0, $1]) => {
12 | a = $0;
13 | seta = $1;
14 | });`;
15 |
16 | it("transform", () => {
17 | const result = transform(code, {
18 | presets: [unisPreset],
19 | });
20 | expect(result?.code).toBe(transformed);
21 | });
22 |
--------------------------------------------------------------------------------
/packages/unis-babel-preset/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "emitDeclarationOnly": true,
5 | "declarationMap": false,
6 | "outDir": "build"
7 | },
8 | "include": ["src"]
9 | }
--------------------------------------------------------------------------------
/packages/unis-core/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 | .parcel-cache
78 |
79 | # Next.js build output
80 | .next
81 |
82 | # Nuxt.js build / generate output
83 | .nuxt
84 | dist
85 | build
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | .vuepress/dist
95 |
96 | # Serverless directories
97 | .serverless/
98 |
99 | # FuseBox cache
100 | .fusebox/
101 |
102 | # DynamoDB Local files
103 | .dynamodb/
104 |
105 | # TernJS port file
106 | .tern-port
107 |
--------------------------------------------------------------------------------
/packages/unis-core/index.d.ts:
--------------------------------------------------------------------------------
1 | import "./jsx-runtime/jsx-runtime";
2 | import "./jsx-runtime/jsx-dev-runtime";
3 | export * from "./dist";
4 |
--------------------------------------------------------------------------------
/packages/unis-core/jsx-runtime/jsx-dev-runtime.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@unis/core/jsx-dev-runtime";
2 |
--------------------------------------------------------------------------------
/packages/unis-core/jsx-runtime/jsx-dev-runtime.js:
--------------------------------------------------------------------------------
1 | const { h2, Fragment } = require("../dist/index.js");
2 |
3 | exports.jsxDEV = h2;
4 | exports.Fragment = Fragment;
5 |
--------------------------------------------------------------------------------
/packages/unis-core/jsx-runtime/jsx-dev-runtime.mjs:
--------------------------------------------------------------------------------
1 | export { h2 as jsxDEV, Fragment } from "../dist/index.mjs";
2 |
--------------------------------------------------------------------------------
/packages/unis-core/jsx-runtime/jsx-runtime.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@unis/core/jsx-runtime";
2 |
--------------------------------------------------------------------------------
/packages/unis-core/jsx-runtime/jsx-runtime.js:
--------------------------------------------------------------------------------
1 | const { h2, Fragment } = require("../dist/index.mjs");
2 |
3 | exports.jsx = h2;
4 | exports.jsxs = h2;
5 | exports.Fragment = Fragment;
6 |
--------------------------------------------------------------------------------
/packages/unis-core/jsx-runtime/jsx-runtime.mjs:
--------------------------------------------------------------------------------
1 | export { h2 as jsx, h2 as jsxs, Fragment } from "../dist/index.mjs";
2 |
--------------------------------------------------------------------------------
/packages/unis-core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@unis/core",
3 | "version": "1.2.5",
4 | "description": "Unis is a simpler and easier to use front-end framework than React",
5 | "main": "dist/index.js",
6 | "module": "dist/index.mjs",
7 | "types": "index.d.ts",
8 | "typings": "index.d.ts",
9 | "exports": {
10 | ".": {
11 | "require": "./dist/index.js",
12 | "import": "./dist/index.mjs"
13 | },
14 | "./jsx-runtime": {
15 | "require": "./jsx-runtime/jsx-runtime.js",
16 | "import": "./jsx-runtime/jsx-runtime.mjs"
17 | },
18 | "./jsx-dev-runtime": {
19 | "require": "./jsx-runtime/jsx-dev-runtime.js",
20 | "import": "./jsx-runtime/jsx-dev-runtime.mjs"
21 | }
22 | },
23 | "scripts": {
24 | "build": "rimraf build && rimraf dist && tsc -p tsconfig.build.json && rollup --config",
25 | "build:dev": "cross-env NODE_ENV=development pnpm build",
26 | "test": "vitest run --coverage",
27 | "test:watch": "vitest -w"
28 | },
29 | "repository": {
30 | "type": "git",
31 | "url": "git+https://github.com/anuoua/unis.git"
32 | },
33 | "keywords": [
34 | "frontend",
35 | "web",
36 | "framwork"
37 | ],
38 | "files": [
39 | "dist",
40 | "jsx-runtime",
41 | "index.d.ts"
42 | ],
43 | "author": "anuoua",
44 | "license": "MIT",
45 | "bugs": {
46 | "url": "https://github.com/anuoua/unis/issues"
47 | },
48 | "homepage": "https://github.com/anuoua/unis#readme",
49 | "dependencies": {
50 | "@types/prop-types": "^15.7.5",
51 | "@types/scheduler": "^0.16.3",
52 | "csstype": "^3.1.2"
53 | },
54 | "devDependencies": {
55 | "@rollup/plugin-node-resolve": "^15.0.2",
56 | "@types/jsdom": "^21.1.1",
57 | "@vitest/coverage-c8": "^0.29.8",
58 | "cross-env": "^7.0.3",
59 | "esbuild": "^0.17.15",
60 | "jsdom": "^21.1.1",
61 | "rimraf": "^4.4.1",
62 | "rollup": "^3.20.2",
63 | "rollup-plugin-dts": "^5.3.0",
64 | "rollup-plugin-esbuild": "^5.0.0",
65 | "tslib": "^2.5.0",
66 | "typescript": "^5.0.3",
67 | "vite": "^4.2.1",
68 | "vitest": "^0.29.8"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/packages/unis-core/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import dts from "rollup-plugin-dts";
2 | import esbuild from "rollup-plugin-esbuild";
3 | import { defineConfig } from "rollup";
4 | import { nodeResolve } from "@rollup/plugin-node-resolve";
5 |
6 | const configGen = (format) =>
7 | defineConfig({
8 | input: "src/index.ts",
9 | output: [
10 | {
11 | dir: "dist",
12 | entryFileNames: `index.${format === "esm" ? "mjs" : "js"}`,
13 | format,
14 | sourcemap: true,
15 | },
16 | ],
17 | plugins: [
18 | nodeResolve(),
19 | esbuild({
20 | sourceMap: true,
21 | target: "esnext",
22 | }),
23 | ],
24 | });
25 |
26 | const dtsRollup = () =>
27 | defineConfig({
28 | input: "build/index.d.ts",
29 | output: [{ file: `dist/index.d.ts`, format: "es" }],
30 | plugins: [dts()],
31 | });
32 |
33 | const config = [configGen("cjs"), configGen("esm"), dtsRollup()];
34 |
35 | export default config;
36 |
--------------------------------------------------------------------------------
/packages/unis-core/src/api/use.ts:
--------------------------------------------------------------------------------
1 | import { getWF } from "./utils";
2 |
3 | export function use any>(fn: T): ReturnType;
4 | export function use any>(
5 | fn: T,
6 | raFn: Function
7 | ): ReturnType;
8 | export function use any>(fn: T, raFn?: Function) {
9 | const workingFiber = getWF();
10 | const effect = () => {
11 | const result = fn(getWF());
12 | return raFn?.(result);
13 | };
14 | workingFiber.stateEffects?.push(effect) ??
15 | (workingFiber.stateEffects = [effect]);
16 | return fn(workingFiber) as ReturnType;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/unis-core/src/api/useContext.ts:
--------------------------------------------------------------------------------
1 | import { Context } from "../context";
2 | import { Fiber } from "../fiber";
3 | import { use } from "./use";
4 |
5 | export function useContext(ctx: T) {
6 | return use(contextHof(ctx), arguments[1]);
7 | }
8 |
9 | const contextHof = (context: T) => {
10 | const readContext = (fiber: Fiber): T["initial"] => {
11 | const { dependencyList = [] } = fiber.reconcileState!;
12 | const result = [...dependencyList]
13 | .reverse()
14 | .find((d) => d.context === context);
15 |
16 | if (result) {
17 | if (fiber.dependencies) {
18 | !fiber.dependencies.includes(result) && fiber.dependencies.push(result);
19 | } else {
20 | fiber.dependencies = [result];
21 | }
22 | return result.value;
23 | } else {
24 | return context.initial;
25 | }
26 | };
27 | return (WF: Fiber) => readContext(WF);
28 | };
29 |
--------------------------------------------------------------------------------
/packages/unis-core/src/api/useEffect.ts:
--------------------------------------------------------------------------------
1 | import { Effect, EFFECT_TYPE, getWF } from "./utils";
2 |
3 | export const useEffect = (cb: Effect, depsFn?: () => any[]) => {
4 | const workingFiber = getWF();
5 | cb.depsFn = depsFn;
6 | if (!cb.type) cb.type = EFFECT_TYPE.TICK;
7 | workingFiber.effects?.push(cb) ?? (workingFiber.effects = [cb]);
8 | };
9 |
--------------------------------------------------------------------------------
/packages/unis-core/src/api/useId.ts:
--------------------------------------------------------------------------------
1 | import { Fiber } from "../fiber";
2 | import { getWF } from "./utils";
3 | import { use } from "./use";
4 | import { generateId } from "../utils";
5 |
6 | export const idHof = () => {
7 | let workingFiber = getWF();
8 | if (!workingFiber.id) {
9 | workingFiber.id = generateId();
10 | }
11 | return (WF: Fiber) => WF.id;
12 | };
13 |
14 | export function useId() {
15 | return use(idHof());
16 | }
17 |
--------------------------------------------------------------------------------
/packages/unis-core/src/api/useLayoutEffect.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "./useEffect";
2 | import { Effect, EFFECT_TYPE } from "./utils";
3 |
4 | export const useLayoutEffect = (cb: Effect, depsFn?: () => any[]) => {
5 | cb.type = EFFECT_TYPE.LAYOUT;
6 | return useEffect(cb, depsFn);
7 | };
8 |
--------------------------------------------------------------------------------
/packages/unis-core/src/api/useMemo.ts:
--------------------------------------------------------------------------------
1 | import { use } from "./use";
2 | import { Fiber, MemorizeState } from "../fiber";
3 | import { arraysEqual } from "../utils";
4 | import { addDispatchEffect, linkMemorizeState } from "./useReducer";
5 |
6 | export const memoHof = (
7 | handler: () => T,
8 | depsFn?: () => any[]
9 | ) => {
10 | let freshFiber: Fiber | undefined;
11 | let freshDeps: any[];
12 | let freshMemorizeState: MemorizeState | undefined;
13 |
14 | let memorizeState: MemorizeState = {
15 | value: undefined,
16 | deps: undefined!,
17 | };
18 |
19 | const effect = () => {
20 | memorizeState = freshMemorizeState!;
21 | memorizeState.deps = freshDeps;
22 | freshFiber = undefined;
23 | freshMemorizeState = undefined;
24 | };
25 |
26 | return (WF: Fiber) => {
27 | freshFiber = WF;
28 | freshDeps = depsFn?.() ?? freshDeps;
29 |
30 | addDispatchEffect(freshFiber, effect);
31 |
32 | freshMemorizeState = {
33 | value:
34 | depsFn && arraysEqual(memorizeState.deps, freshDeps)
35 | ? memorizeState.value
36 | : handler(),
37 | deps: memorizeState?.deps,
38 | };
39 |
40 | linkMemorizeState(freshFiber, freshMemorizeState);
41 |
42 | return freshMemorizeState.value as T;
43 | };
44 | };
45 |
46 | export function useMemo(
47 | handler: () => T,
48 | depsFn?: () => any[]
49 | ) {
50 | return use(memoHof(handler, depsFn), arguments[2]);
51 | }
52 |
--------------------------------------------------------------------------------
/packages/unis-core/src/api/useProps.ts:
--------------------------------------------------------------------------------
1 | import { Fiber } from "../fiber";
2 | import { use } from "./use";
3 |
4 | export function useProps(p: T) {
5 | return use(propsHof(p), arguments[1]);
6 | }
7 |
8 | export const propsHof = (props: T) => {
9 | return (WF: Fiber) => WF.props as T;
10 | };
11 |
--------------------------------------------------------------------------------
/packages/unis-core/src/api/useReducer.ts:
--------------------------------------------------------------------------------
1 | import { Effect, markFiber } from "./utils";
2 | import { use } from "./use";
3 | import { Fiber, findRoot, findRuntime, MemorizeState, TokTik } from "../fiber";
4 | import { readyForWork } from "../reconcile";
5 |
6 | export type Reducer = (state: T, action: T2) => T;
7 |
8 | const readyList: (() => Fiber)[] = [];
9 |
10 | const triggerReconcile = () => {
11 | const fibers = new Set(readyList.map((getFiber) => getFiber()));
12 | fibers.forEach(markFiber);
13 |
14 | // multiple app trigger same time
15 | const rootFibers = new Set(Array.from(fibers).map(findRoot));
16 | rootFibers.forEach((fiber) => readyForWork(fiber));
17 |
18 | readyList.length = 0;
19 | };
20 |
21 | export const reducerHof = (
22 | reducerFn: Reducer,
23 | initial: T
24 | ) => {
25 | let currentFiber: Fiber | undefined; // do not getWF here, workingFiber should be assigned in effect.
26 | let freshFiber: Fiber | undefined;
27 | let freshMemorizeState: MemorizeState | undefined;
28 | let toktik: TokTik | undefined;
29 |
30 | let memorizeState: MemorizeState = {
31 | value: undefined,
32 | dispatchValue: initial,
33 | deps: [initial],
34 | };
35 |
36 | const dispatch = (action: T2) => {
37 | if (!currentFiber) return console.warn("Component is not created");
38 | if (currentFiber.isDestroyed)
39 | return console.warn("Component has been destroyed");
40 |
41 | const newState = reducerFn(memorizeState.value, action);
42 | if (Object.is(newState, memorizeState.value)) return;
43 |
44 | memorizeState.dispatchValue = newState;
45 | memorizeState.deps = [newState];
46 |
47 | if (freshFiber) {
48 | toktik!.clearTikTaskQueue();
49 | }
50 |
51 | readyList.push(() => currentFiber!);
52 |
53 | if (readyList.length === 1) {
54 | toktik!.addTok(triggerReconcile, true);
55 | }
56 | };
57 |
58 | const effect: Effect = () => {
59 | currentFiber = freshFiber!;
60 | memorizeState = freshMemorizeState!;
61 | if (!toktik) toktik = findRuntime(currentFiber).toktik;
62 | freshFiber = undefined;
63 | freshMemorizeState = undefined;
64 | };
65 |
66 | return (WF: Fiber) => {
67 | freshFiber = WF;
68 |
69 | addDispatchEffect(freshFiber, effect);
70 |
71 | freshMemorizeState = {
72 | value:
73 | memorizeState.deps.length > 0
74 | ? memorizeState.dispatchValue
75 | : memorizeState.value,
76 | deps: [],
77 | };
78 |
79 | linkMemorizeState(freshFiber, freshMemorizeState);
80 |
81 | return [freshMemorizeState.value, dispatch] as const;
82 | };
83 | };
84 |
85 | export function useReducer(reducerFn: Reducer, initial: T) {
86 | return use(reducerHof(reducerFn, initial), arguments[2]);
87 | }
88 |
89 | export const addDispatchEffect = (freshFiber: Fiber, effect: Effect) => {
90 | freshFiber.reconcileState!.dispatchEffectList?.push(effect) ??
91 | (freshFiber.reconcileState!.dispatchEffectList = [effect]);
92 | };
93 |
94 | export const linkMemorizeState = (
95 | freshFiber: Fiber,
96 | freshMemorizeState: MemorizeState
97 | ) => {
98 | if (freshFiber.memorizeState) {
99 | const first = freshFiber.memorizeState.next;
100 | freshFiber.memorizeState.next = freshMemorizeState;
101 | freshMemorizeState.next = first;
102 | } else {
103 | freshFiber.memorizeState = freshMemorizeState;
104 | freshMemorizeState.next = freshMemorizeState;
105 | }
106 | };
107 |
108 | export const cutMemorizeState = (fiber: Fiber) => {
109 | const first = fiber.memorizeState?.next;
110 | fiber.memorizeState && (fiber.memorizeState.next = undefined);
111 | fiber.memorizeState = first;
112 | };
113 |
--------------------------------------------------------------------------------
/packages/unis-core/src/api/useRef.ts:
--------------------------------------------------------------------------------
1 | export interface Ref {
2 | current: T;
3 | }
4 |
5 | export function useRef(): Ref;
6 | export function useRef(value: T): Ref;
7 | export function useRef(value?: T) {
8 | return { current: value };
9 | }
10 |
--------------------------------------------------------------------------------
/packages/unis-core/src/api/useState.ts:
--------------------------------------------------------------------------------
1 | import { use } from "./use";
2 | import { reducerHof } from "./useReducer";
3 |
4 | export const stateHof = (initial: T) => {
5 | return reducerHof((preState, action) => action, initial);
6 | };
7 |
8 | export function useState(): [
9 | T | undefined,
10 | (value: T | undefined) => void
11 | ];
12 | export function useState(initial: T): [T, (value: T) => void];
13 | export function useState(initial?: T) {
14 | return use(stateHof(initial), arguments[1]);
15 | }
16 |
--------------------------------------------------------------------------------
/packages/unis-core/src/api/utils.ts:
--------------------------------------------------------------------------------
1 | import { Fiber, FLAG, mergeFlag } from "../fiber";
2 | import { getWorkingFiber } from "../reconcile";
3 | import { arraysEqual } from "../utils";
4 |
5 | export enum EFFECT_TYPE {
6 | LAYOUT = "layout",
7 | TICK = "tick",
8 | }
9 |
10 | export type Effect = (() => (() => void) | void) & {
11 | type?: EFFECT_TYPE;
12 | clear?: (() => void) | void;
13 | depsFn?: () => any;
14 | deps?: any;
15 | };
16 |
17 | export const getWF = (): Fiber | never => {
18 | const workingFiber = getWorkingFiber();
19 | if (workingFiber) {
20 | return workingFiber;
21 | } else {
22 | throw Error("Do not call use function outside of component");
23 | }
24 | };
25 |
26 | export const markFiber = (workingFiber: Fiber) => {
27 | workingFiber.flag = mergeFlag(workingFiber.flag, FLAG.UPDATE);
28 |
29 | let iFiber: Fiber | undefined = workingFiber;
30 |
31 | while ((iFiber = iFiber.parent)) {
32 | if (iFiber.childFlag) break;
33 | iFiber.childFlag = mergeFlag(iFiber.childFlag, FLAG.UPDATE);
34 | }
35 | };
36 |
37 | export const runStateEffects = (fiber: Fiber) => {
38 | for (const effect of fiber.stateEffects ?? []) {
39 | effect();
40 | }
41 | };
42 |
43 | export const effectDepsEqual = (effect: Effect) => {
44 | const deps = effect.depsFn?.();
45 | const equal = arraysEqual(deps, effect.deps);
46 | effect.deps = deps;
47 | return equal;
48 | };
49 |
50 | export const clearEffects = (effects?: Effect[]) => {
51 | if (!effects) return;
52 | for (const effect of effects) {
53 | effect.clear?.();
54 | }
55 | };
56 |
57 | export const runEffects = (effects?: Effect[]) => {
58 | if (!effects) return;
59 | for (const effect of effects) {
60 | effect.clear = effect();
61 | }
62 | };
63 |
64 | export const clearAndRunEffects = (effects?: Effect[]) => {
65 | if (!effects) return;
66 | for (const effect of effects) {
67 | if (effectDepsEqual(effect)) continue;
68 | effect.clear?.();
69 | effect.clear = effect();
70 | }
71 | };
72 |
--------------------------------------------------------------------------------
/packages/unis-core/src/commit.ts:
--------------------------------------------------------------------------------
1 | import { clearEffects } from "./api/utils";
2 | import {
3 | Fiber,
4 | findEls,
5 | FLAG,
6 | getContainerElFiber,
7 | graft,
8 | isComponent,
9 | isHostElement,
10 | matchFlag,
11 | isText,
12 | isElement,
13 | ReconcileState,
14 | isPortal,
15 | Operator,
16 | } from "./fiber";
17 |
18 | export const commitDeletion = (fiber: Fiber, operator: Operator) => {
19 | let iFiber: Fiber | undefined = fiber;
20 |
21 | while (iFiber) {
22 | if (isHostElement(iFiber)) {
23 | iFiber.props.ref && (iFiber.props.ref.current = undefined);
24 | }
25 | if (isComponent(iFiber)) {
26 | clearEffects(iFiber.effects);
27 | }
28 | /**
29 | * remove input element may trigger blur sync event,
30 | * so isDestroyed must be true before remove to prevent dispatch in useReducer.
31 | */
32 | iFiber.isDestroyed = true;
33 | if (isPortal(iFiber)) {
34 | iFiber.child && operator.remove(iFiber.child);
35 | }
36 | iFiber.dependencies = undefined;
37 | iFiber.reconcileState = undefined;
38 |
39 | if (iFiber.child) {
40 | iFiber = iFiber.child;
41 | continue;
42 | } else if (iFiber === fiber) {
43 | iFiber = undefined;
44 | continue;
45 | }
46 |
47 | while (iFiber) {
48 | if (iFiber.sibling) {
49 | iFiber = iFiber.sibling;
50 | break;
51 | }
52 |
53 | if (iFiber.parent !== fiber) {
54 | iFiber = iFiber.parent;
55 | } else {
56 | iFiber = undefined;
57 | break;
58 | }
59 | }
60 | }
61 |
62 | operator.remove(fiber);
63 | };
64 |
65 | export const commitUpdate = (fiber: Fiber, operator: Operator) => {
66 | if (isText(fiber)) operator.updateTextProperties(fiber);
67 | if (isHostElement(fiber)) operator.updateElementProperties(fiber);
68 | };
69 |
70 | export const commitInsert = (fiber: Fiber, operator: Operator) => {
71 | const container = getContainerElFiber(fiber)!;
72 |
73 | const insertElements = isElement(fiber)
74 | ? [fiber.el!]
75 | : findEls(
76 | matchFlag(fiber.commitFlag, FLAG.REUSE) ? fiber.alternate! : fiber
77 | );
78 |
79 | const insertTarget = isPortal(container)
80 | ? null
81 | : fiber.preElFiber
82 | ? operator.nextSibling(fiber.preElFiber)
83 | : operator.firstChild(container);
84 |
85 | for (const insertElement of insertElements) {
86 | operator.insertBefore(container, insertElement, insertTarget);
87 | }
88 | };
89 |
90 | export const commit = (reconcileState: ReconcileState) => {
91 | const { operator } = reconcileState.rootWorkingFiber.runtime!;
92 | for (const fiber of reconcileState.commitList) {
93 | if (matchFlag(fiber.commitFlag, FLAG.DELETE)) {
94 | commitDeletion(fiber.alternate!, operator);
95 | continue;
96 | }
97 | if (matchFlag(fiber.commitFlag, FLAG.UPDATE)) {
98 | commitUpdate(fiber, operator);
99 | }
100 | if (matchFlag(fiber.commitFlag, FLAG.INSERT)) {
101 | commitInsert(fiber, operator);
102 | }
103 | if (matchFlag(fiber.commitFlag, FLAG.REUSE)) {
104 | graft(fiber, fiber.alternate!);
105 | }
106 | fiber.preElFiber = undefined;
107 | fiber.alternate = undefined;
108 | fiber.commitFlag = undefined;
109 | }
110 | };
111 |
--------------------------------------------------------------------------------
/packages/unis-core/src/context.ts:
--------------------------------------------------------------------------------
1 | import { useProps } from "./api/useProps";
2 | import { useContext } from "./api/useContext";
3 | import { PROVIDER, Fiber } from "./fiber";
4 |
5 | export interface Context {
6 | Provider: (props: { value: T; children: any }) => JSX.Element;
7 | Consumer: (props: { children: (value: T) => JSX.Element }) => JSX.Element;
8 | initial: T;
9 | }
10 |
11 | export interface Dependency {
12 | context: Context;
13 | value: T;
14 | }
15 |
16 | const providerContextMap = new WeakMap();
17 |
18 | export const createDependency = (fiber: Fiber) => {
19 | return {
20 | context: providerContextMap.get(fiber.tag as Function)!,
21 | value: fiber.props.value,
22 | };
23 | };
24 |
25 | export const findDependency = (fiber: Fiber, contextFiber: Fiber) =>
26 | fiber.dependencies?.find(
27 | (dependency) =>
28 | dependency.context ===
29 | providerContextMap.get(contextFiber.tag as Function)
30 | );
31 |
32 | export function createContext(initial: T): Context;
33 | export function createContext(initial: T) {
34 | const Provider = (props: { value: T; children: any }) => props.children;
35 |
36 | Provider.take = {
37 | type: PROVIDER,
38 | };
39 |
40 | const Consumer = (props: { children: (value: T) => JSX.Element }) => {
41 | let p = useProps(
42 | props,
43 | // @ts-ignore
44 | ($) => (p = $)
45 | );
46 | let state = useContext(
47 | context,
48 | // @ts-ignore
49 | ($) => (state = $)
50 | );
51 | return () => p.children(state);
52 | };
53 |
54 | const context: Context = {
55 | Provider,
56 | Consumer,
57 | initial,
58 | };
59 |
60 | providerContextMap.set(Provider, context);
61 |
62 | return context;
63 | }
64 |
--------------------------------------------------------------------------------
/packages/unis-core/src/createTokTik.ts:
--------------------------------------------------------------------------------
1 | export type Task = Function & { isTok?: any };
2 |
3 | export const createTokTik = (options: {
4 | nextTick: (cb: VoidFunction, pending: boolean) => void;
5 | now: () => number;
6 | interval?: number;
7 | }) => {
8 | const { nextTick, now, interval = 4 } = options;
9 |
10 | const timeSlicing = !!interval;
11 |
12 | let lastTime: number = 0;
13 | let looping = false;
14 |
15 | const tikQueue: Task[] = [];
16 | const tokQueue: Task[] = [];
17 |
18 | const next = () => tikQueue[0] ?? tokQueue[0];
19 |
20 | const pick = () => tikQueue.shift() ?? tokQueue.shift();
21 |
22 | const loop = (task: Task): void => {
23 | looping = true;
24 | runTask(task);
25 | const nextTask = next();
26 | if (nextTask) {
27 | if (shouldYield() || nextTask.isTok) {
28 | nextTick(() => loop(pick()!), nextTask.isTok);
29 | } else {
30 | loop(pick()!);
31 | }
32 | return;
33 | }
34 | looping = false;
35 | };
36 |
37 | const runTask = timeSlicing
38 | ? (task: Task) => {
39 | lastTime = now();
40 | return task();
41 | }
42 | : (task: Task) => task();
43 |
44 | const addTok = (task: Task, pending = false) => {
45 | task.isTok = true;
46 | looping
47 | ? tokQueue.push(task)
48 | : pending
49 | ? nextTick(() => loop(task), pending)
50 | : loop(task);
51 | };
52 |
53 | const addTik = (task: Task) => {
54 | looping && tikQueue.push(task);
55 | };
56 |
57 | const clearTikTaskQueue = () => (tikQueue.length = 0);
58 |
59 | const shouldYield = timeSlicing
60 | ? () => now() - lastTime > interval
61 | : () => false;
62 |
63 | return {
64 | addTok,
65 | addTik,
66 | clearTikTaskQueue,
67 | shouldYield,
68 | };
69 | };
70 |
--------------------------------------------------------------------------------
/packages/unis-core/src/diff.ts:
--------------------------------------------------------------------------------
1 | import {
2 | clearFlag,
3 | Fiber,
4 | FLAG,
5 | isElement,
6 | matchFlag,
7 | isPortal,
8 | isSame,
9 | mergeFlag,
10 | isComponent,
11 | ReconcileState,
12 | isText,
13 | findToRoot,
14 | } from "./fiber";
15 | import {
16 | classes,
17 | isNullish,
18 | isStr,
19 | isEvent,
20 | keys,
21 | styleStr,
22 | svgKey,
23 | } from "./utils";
24 |
25 | export type AttrDiff = [string, any, any][];
26 |
27 | export const attrDiff = (
28 | newFiber: Record,
29 | oldFiber: Record,
30 | onlyEvent = false
31 | ) => {
32 | const diff: AttrDiff = [];
33 | const newProps = newFiber.props;
34 | const oldProps = oldFiber.props;
35 |
36 | const getRealAttr = (attr: string) => {
37 | if (attr === "className") return "class";
38 | if (attr === "htmlFor") return "for";
39 | if (newFiber.isSvg) return svgKey(attr);
40 | return attr.toLowerCase();
41 | };
42 |
43 | const getRealValue = (newValue: any, key: string) => {
44 | if (isNullish(newValue)) return;
45 | switch (key) {
46 | case "className":
47 | return isStr(newValue) ? newValue : classes(newValue);
48 | case "style":
49 | return isStr(newValue)
50 | ? newValue
51 | : styleStr(newValue as Partial);
52 | default:
53 | return newValue;
54 | }
55 | };
56 |
57 | for (const key of keys({ ...newProps, ...oldProps })) {
58 | if (onlyEvent && !isEvent(key)) continue;
59 | if (["xmlns", "children"].includes(key)) continue;
60 | const newValue = newProps[key];
61 | const oldValue = oldProps[key];
62 | const realNewValue = getRealValue(newValue, key);
63 | const realOldValue = getRealValue(oldValue, key);
64 | if (
65 | !isNullish(newValue) &&
66 | !isNullish(oldValue) &&
67 | realNewValue === realOldValue
68 | )
69 | continue;
70 | diff.push([getRealAttr(key), realNewValue, realOldValue]);
71 | }
72 |
73 | return diff;
74 | };
75 |
76 | export const clone = (newFiber: Fiber, oldFiber: Fiber, commitFlag?: FLAG) =>
77 | Object.assign(
78 | {
79 | ...newFiber,
80 | commitFlag,
81 | alternate: oldFiber,
82 | },
83 | isComponent(newFiber)
84 | ? {
85 | renderFn: oldFiber.renderFn,
86 | rendered: oldFiber.rendered,
87 | stateEffects: oldFiber.stateEffects,
88 | effects: oldFiber.effects,
89 | id: oldFiber.id,
90 | }
91 | : isElement(newFiber)
92 | ? { el: oldFiber.el, isSvg: oldFiber.isSvg }
93 | : isPortal(newFiber)
94 | ? { to: oldFiber.to }
95 | : undefined
96 | );
97 |
98 | export const reuse = (newFiber: Fiber, oldFiber: Fiber, commitFlag?: FLAG) => ({
99 | ...newFiber,
100 | commitFlag,
101 | alternate: oldFiber,
102 | });
103 |
104 | export const del = (oldFiber: Fiber): Fiber => ({
105 | commitFlag: FLAG.DELETE,
106 | alternate: oldFiber,
107 | });
108 |
109 | export const create = (
110 | newFiber: Fiber,
111 | parentFiber: Fiber,
112 | hydrate = false
113 | ) => {
114 | const retFiber = {
115 | ...newFiber,
116 | commitFlag: matchFlag(parentFiber.commitFlag, FLAG.CREATE)
117 | ? FLAG.CREATE
118 | : FLAG.CREATE | FLAG.INSERT,
119 | } as Fiber;
120 |
121 | if (isElement(newFiber)) {
122 | retFiber.isSvg = newFiber.tag === "svg" || parentFiber.isSvg;
123 | if (!hydrate) {
124 | retFiber.attrDiff = isText(retFiber)
125 | ? undefined
126 | : attrDiff(retFiber, { props: {} });
127 | }
128 | }
129 |
130 | if (isPortal(newFiber)) {
131 | retFiber.commitFlag = undefined;
132 | }
133 |
134 | return retFiber;
135 | };
136 |
137 | export const keyIndexMapGen = (
138 | children: Fiber[],
139 | start: number,
140 | end: number
141 | ) => {
142 | const map: any = {};
143 | for (let i = start; i <= end; i++) {
144 | const key = children[i].props?.key;
145 | if (key !== undefined) map[key] = i;
146 | }
147 | return map;
148 | };
149 |
150 | const determineCommitFlag = (
151 | parentFiber: Fiber,
152 | newFiber: Fiber,
153 | oldFiber: Fiber,
154 | flag?: FLAG
155 | ) => {
156 | /**
157 | * the nearest parent component fiber
158 | */
159 | const nearestComponent = isComponent(parentFiber)
160 | ? parentFiber
161 | : findToRoot(parentFiber, (fiber) => isComponent(fiber));
162 |
163 | /**
164 | * when nearest parent component fiber with FLAG.UPDATE commitFlag, it should be FLAG.UPDATE.
165 | */
166 | let commitFlag =
167 | !matchFlag(nearestComponent?.commitFlag, FLAG.UPDATE) &&
168 | parentFiber.alternate!.childFlag
169 | ? !oldFiber.childFlag && !oldFiber.flag
170 | ? FLAG.REUSE
171 | : oldFiber.flag
172 | : FLAG.UPDATE;
173 |
174 | /**
175 | * when memo fiber compare result is true, it should be FLAG.REUSE.
176 | */
177 | if (
178 | isComponent(oldFiber) &&
179 | !oldFiber.childFlag &&
180 | !oldFiber.flag &&
181 | (oldFiber.tag as Function & { compare?: Function }).compare?.(
182 | newFiber.props,
183 | oldFiber.props
184 | )
185 | ) {
186 | commitFlag = mergeFlag(commitFlag, FLAG.REUSE);
187 | }
188 |
189 | if (isElement(newFiber) && matchFlag(commitFlag, FLAG.UPDATE)) {
190 | let diff = attrDiff(newFiber, oldFiber);
191 | if (diff.length) {
192 | newFiber.attrDiff = diff;
193 | } else {
194 | commitFlag = clearFlag(commitFlag, FLAG.UPDATE);
195 | }
196 | }
197 |
198 | flag && (commitFlag = mergeFlag(commitFlag, flag));
199 |
200 | /**
201 | * portal don't need commitFlag
202 | */
203 | if (isPortal(newFiber)) {
204 | commitFlag = undefined;
205 | }
206 |
207 | if (matchFlag(commitFlag, FLAG.REUSE)) {
208 | commitFlag = clearFlag(commitFlag, FLAG.UPDATE);
209 | }
210 |
211 | return commitFlag;
212 | };
213 |
214 | const getSameNewFiber = (
215 | parentFiber: Fiber,
216 | newFiber: Fiber,
217 | oldFiber: Fiber,
218 | flag?: FLAG
219 | ) => {
220 | const commitFlag = determineCommitFlag(parentFiber, newFiber, oldFiber, flag);
221 | return matchFlag(commitFlag, FLAG.REUSE)
222 | ? reuse(newFiber, oldFiber, commitFlag)
223 | : clone(newFiber, oldFiber, commitFlag);
224 | };
225 |
226 | export const diff = (
227 | parentFiber: Fiber,
228 | oldChildren: Fiber[] = [],
229 | newChildren: Fiber[] = []
230 | ) => {
231 | const { reconcileState } = parentFiber as { reconcileState: ReconcileState };
232 |
233 | let cloneChildren: Fiber[] = [];
234 | let newStartIndex = 0;
235 | let newEndIndex = newChildren.length - 1;
236 | let oldStartIndex = 0;
237 | let oldEndIndex = oldChildren.length - 1;
238 |
239 | let newStartFiber = newChildren[newStartIndex];
240 | let newEndFiber = newChildren[newEndIndex];
241 | let oldStartFiber = oldChildren[oldStartIndex];
242 | let oldEndFiber = oldChildren[oldEndIndex];
243 |
244 | let preStartFiber: Fiber | undefined;
245 | let preEndFiber: Fiber | undefined;
246 |
247 | const deletion = (fiber: Fiber) => {
248 | reconcileState.commitList.push(del(fiber));
249 | };
250 |
251 | const forward = () => {
252 | if (preStartFiber) preStartFiber.sibling = newStartFiber;
253 | newStartFiber.parent = parentFiber;
254 | newStartFiber.index = newStartIndex;
255 | newStartFiber.reconcileState = reconcileState;
256 | preStartFiber = newStartFiber;
257 | cloneChildren[newStartIndex] = newStartFiber;
258 | newStartFiber = newChildren[++newStartIndex];
259 | };
260 |
261 | const forwardEnd = () => {
262 | if (preEndFiber) newEndFiber.sibling = preEndFiber;
263 | newEndFiber.parent = parentFiber;
264 | newEndFiber.index = newEndIndex;
265 | newEndFiber.reconcileState = reconcileState;
266 | preEndFiber = newEndFiber;
267 | cloneChildren[newEndIndex] = newEndFiber;
268 | newEndFiber = newChildren[--newEndIndex];
269 | };
270 |
271 | const oldForward = () => {
272 | oldStartFiber = oldChildren[++oldStartIndex];
273 | };
274 |
275 | const oldForwardEnd = () => {
276 | oldEndFiber = oldChildren[--oldEndIndex];
277 | };
278 |
279 | let keyIndexMap: any;
280 |
281 | while (newStartIndex <= newEndIndex && oldStartIndex <= oldEndIndex) {
282 | if (oldStartFiber === undefined) {
283 | oldForward();
284 | } else if (oldEndFiber === undefined) {
285 | oldForwardEnd();
286 | } else if (isSame(newStartFiber, oldStartFiber)) {
287 | newStartFiber = getSameNewFiber(
288 | parentFiber,
289 | newStartFiber,
290 | oldStartFiber
291 | );
292 | forward();
293 | oldForward();
294 | } else if (isSame(newEndFiber, oldEndFiber)) {
295 | newEndFiber = getSameNewFiber(parentFiber, newEndFiber, oldEndFiber);
296 | forwardEnd();
297 | oldForwardEnd();
298 | } else if (isSame(newStartFiber, oldEndFiber)) {
299 | newStartFiber = getSameNewFiber(
300 | parentFiber,
301 | newStartFiber,
302 | oldEndFiber,
303 | FLAG.INSERT
304 | );
305 | forward();
306 | oldForwardEnd();
307 | } else if (isSame(newEndFiber, oldStartFiber)) {
308 | newEndFiber = getSameNewFiber(
309 | parentFiber,
310 | newEndFiber,
311 | oldStartFiber,
312 | FLAG.INSERT
313 | );
314 | forwardEnd();
315 | oldForward();
316 | } else {
317 | if (!keyIndexMap) {
318 | keyIndexMap = keyIndexMapGen(oldChildren, oldStartIndex, oldEndIndex);
319 | }
320 | const index = keyIndexMap[newStartFiber.props.key];
321 | if (isNaN(index)) {
322 | newStartFiber = create(newStartFiber, parentFiber);
323 | } else {
324 | const targetFiber = oldChildren[index];
325 | if (isSame(newStartFiber, targetFiber)) {
326 | newStartFiber = getSameNewFiber(
327 | parentFiber,
328 | newStartFiber,
329 | targetFiber,
330 | FLAG.INSERT
331 | );
332 | oldChildren[index] = undefined as unknown as Fiber;
333 | } else {
334 | newStartFiber = create(newStartFiber, parentFiber);
335 | }
336 | }
337 | forward();
338 | }
339 | }
340 |
341 | if (oldStartIndex > oldEndIndex) {
342 | newChildren.slice(newStartIndex, newEndIndex + 1).forEach((fiber) => {
343 | newStartFiber = create(
344 | newStartFiber,
345 | parentFiber,
346 | reconcileState.hydrate
347 | );
348 | forward();
349 | });
350 | } else if (newStartIndex > newEndIndex) {
351 | oldChildren
352 | .slice(oldStartIndex, oldEndIndex + 1)
353 | .forEach((fiber) => fiber && deletion(fiber));
354 | }
355 |
356 | if (preStartFiber && preEndFiber) preStartFiber.sibling = preEndFiber;
357 |
358 | parentFiber.child = cloneChildren[0];
359 | parentFiber.children = cloneChildren;
360 | };
361 |
--------------------------------------------------------------------------------
/packages/unis-core/src/fiber.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from "./api/utils";
2 | import { Dependency } from "./context";
3 | import { AttrDiff } from "./diff";
4 | import { isFun, isNullish } from "./utils";
5 |
6 | export interface ReconcileState {
7 | rootWorkingFiber: Fiber;
8 | dispatchEffectList: Effect[];
9 | commitList: Fiber[];
10 | dependencyList: Dependency[];
11 | workingPreElFiber?: Fiber;
12 | hydrate: boolean;
13 | hydrateEl?: FiberEl;
14 | }
15 |
16 | export enum FLAG {
17 | CREATE = 1 << 1,
18 | INSERT = 1 << 2,
19 | UPDATE = 1 << 3,
20 | DELETE = 1 << 4,
21 | REUSE = 1 << 5,
22 | }
23 |
24 | export type FlagName = "flag" | "childFlag" | "commitFlag";
25 |
26 | export const mergeFlag = (a: FLAG | undefined, b: FLAG) =>
27 | isNullish(a) ? b : a | b;
28 |
29 | export const clearFlag = (a: FLAG | undefined, b: FLAG) =>
30 | isNullish(a) ? a : a & ~b;
31 |
32 | export const matchFlag = (a: FLAG | undefined, b: FLAG) =>
33 | isNullish(a) ? false : a & b;
34 |
35 | export type FiberEl = unknown;
36 | export type FiberType =
37 | | string
38 | | Function
39 | | Symbol
40 | | ((...p: any[]) => () => any);
41 |
42 | export interface MemorizeState {
43 | value: any;
44 | dispatchValue?: any;
45 | deps: any[];
46 | next?: MemorizeState;
47 | }
48 |
49 | export interface TokTik {
50 | addTok: (task: Function, pending?: boolean) => void;
51 | addTik: (task: Function) => void;
52 | clearTikTaskQueue: () => void;
53 | shouldYield: () => boolean;
54 | }
55 |
56 | export interface Operator {
57 | // for reuse element when hydrate
58 | nextElement(el: FiberEl): FiberEl | null;
59 |
60 | // for reuse element when hydrate
61 | matchElement(fiber: Fiber, el: FiberEl): boolean;
62 |
63 | createElement(fiber: Fiber): FiberEl;
64 |
65 | remove(fiber: Fiber): void;
66 |
67 | insertBefore(
68 | containerFiber: Fiber,
69 | insertElement: FiberEl,
70 | targetElement: FiberEl | null
71 | ): void;
72 |
73 | firstChild(fiber: Fiber): FiberEl | null;
74 |
75 | nextSibling(fiber: Fiber): FiberEl | null;
76 |
77 | updateTextProperties(fiber: Fiber): void;
78 |
79 | updateElementProperties(fiber: Fiber): void;
80 | }
81 |
82 | export interface Runtime {
83 | toktik: TokTik;
84 | operator: Operator;
85 | }
86 |
87 | export interface Fiber {
88 | id?: string;
89 | parent?: Fiber;
90 | child?: Fiber;
91 | sibling?: Fiber;
92 | index?: number;
93 | to?: FiberEl;
94 | el?: FiberEl;
95 | preElFiber?: Fiber;
96 | isSvg?: boolean;
97 | isDestroyed?: boolean;
98 | props?: any;
99 | compare?: Function;
100 | attrDiff?: AttrDiff;
101 | alternate?: Fiber;
102 | tag?: string | Function;
103 | type?: Symbol;
104 | renderFn?: Function;
105 | rendered?: any;
106 | flag?: FLAG;
107 | childFlag?: FLAG;
108 | commitFlag?: FLAG;
109 | children?: Fiber[];
110 | stateEffects?: Effect[];
111 | effects?: Effect[];
112 | dependencies?: Dependency[];
113 | reconcileState?: ReconcileState;
114 | memorizeState?: MemorizeState;
115 | runtime?: Runtime;
116 | }
117 |
118 | export const createFiber = (options: Partial = {}) =>
119 | Object.assign(
120 | {
121 | id: undefined,
122 | parent: undefined,
123 | child: undefined,
124 | sibling: undefined,
125 | index: undefined,
126 | to: undefined,
127 | el: undefined,
128 | preElFiber: undefined,
129 | isSvg: undefined,
130 | isDestroyed: undefined,
131 | props: undefined,
132 | compare: undefined,
133 | attrDiff: undefined,
134 | alternate: undefined,
135 | tag: undefined,
136 | type: undefined,
137 | renderFn: undefined,
138 | rendered: undefined,
139 | flag: undefined,
140 | childFlag: undefined,
141 | commitFlag: undefined,
142 | children: undefined,
143 | stateEffects: undefined,
144 | effects: undefined,
145 | dependencies: undefined,
146 | reconcileState: undefined,
147 | memorizeState: undefined,
148 | },
149 | options
150 | );
151 |
152 | export const TEXT = Symbol("$$Text");
153 | export const ELEMENT = Symbol("$$Element");
154 | export const PORTAL = Symbol("$$Portal");
155 | export const PROVIDER = Symbol("$$Provider");
156 | export const COMPONENT = Symbol("$$Component");
157 |
158 | export const isText = (fiber: Fiber) => fiber.type === TEXT;
159 | export const isHostElement = (fiber: Fiber) => fiber.type === ELEMENT;
160 | export const isElement = (fiber: Fiber) =>
161 | isHostElement(fiber) || isText(fiber);
162 |
163 | export const isPortal = (fiber: Fiber) => fiber.type === PORTAL;
164 | export const isProvider = (fiber: Fiber) => fiber.type === PROVIDER;
165 | export const isCustomComponent = (fiber: Fiber) => fiber.type === COMPONENT;
166 | export const isComponent = (fiber: Fiber) => isFun(fiber.tag);
167 |
168 | export const isSame = (fiber1?: Fiber, fiber2?: Fiber) =>
169 | fiber1 &&
170 | fiber2 &&
171 | fiber1.tag === fiber2.tag &&
172 | fiber1.props?.key === fiber2.props?.key;
173 |
174 | export interface WalkHook {
175 | enter?: (currentFiber: Fiber, skipChild: boolean) => any;
176 | down?: (currentFiber: Fiber, nextFiber: Fiber) => any;
177 | sibling?: (currentFiber: Fiber, nextFiber?: Fiber) => any;
178 | up?: (currentFiber: Fiber, nextFiber?: Fiber) => any;
179 | return?: (currentFiber?: Fiber) => any;
180 | }
181 |
182 | export type WalkHookKeys = keyof WalkHook;
183 |
184 | export type WalkHookList = {
185 | [K in keyof WalkHook]: WalkHook[K][];
186 | };
187 |
188 | export const createNext = () => {
189 | const walkHooks: WalkHookList = {};
190 |
191 | const addHook = (walkHook: WalkHook) => {
192 | Object.entries(walkHook).forEach(([key, value]) => {
193 | const list = walkHooks[key as WalkHookKeys];
194 | list ? list.push(value) : (walkHooks[key as WalkHookKeys] = [value]);
195 | });
196 | };
197 |
198 | const runWalkHooks = (
199 | key: T,
200 | ...args: Parameters[T]>
201 | ) => {
202 | return walkHooks[key]?.map((hook) => hook!(...(args as [any, any])));
203 | };
204 |
205 | const next = (fiber: Fiber, skipChild = false): Fiber | undefined => {
206 | if (runWalkHooks("enter", fiber, skipChild)?.includes(false)) return;
207 | const { child } = fiber;
208 | let nextFiber: Fiber | undefined = fiber;
209 | if (child && !skipChild) {
210 | runWalkHooks("down", nextFiber, child);
211 | nextFiber = child;
212 | } else {
213 | while (nextFiber) {
214 | const { sibling, parent } = nextFiber as Fiber;
215 | if (sibling) {
216 | runWalkHooks("sibling", nextFiber, sibling);
217 | nextFiber = sibling;
218 | break;
219 | }
220 | if (runWalkHooks("up", nextFiber, parent)?.includes(false)) {
221 | nextFiber = undefined;
222 | break;
223 | }
224 | nextFiber = parent;
225 | }
226 | }
227 | runWalkHooks("return", nextFiber);
228 | return nextFiber;
229 | };
230 |
231 | return [next, addHook] as const;
232 | };
233 |
234 | export const graft = (newFiber: Fiber, oldFiber: Fiber) => {
235 | const parent = newFiber.parent!;
236 | const parentChildren = parent.children!;
237 | const index = newFiber.index!;
238 | const preIndex = index - 1;
239 |
240 | if (index === 0) parent.child = oldFiber;
241 | if (preIndex >= 0) parentChildren[preIndex].sibling = oldFiber;
242 |
243 | parentChildren[index] = oldFiber;
244 |
245 | oldFiber.sibling = newFiber.sibling;
246 | oldFiber.parent = parent;
247 | };
248 |
249 | export const findEls = (fiber: Fiber, findInPortal = false) => {
250 | const els: FiberEl[] = [];
251 | isElement(fiber)
252 | ? els.push(fiber.el!)
253 | : isPortal(fiber) && !findInPortal
254 | ? false
255 | : fiber.children?.forEach((child) => {
256 | els.push(...findEls(child, findInPortal));
257 | });
258 |
259 | return els;
260 | };
261 |
262 | export const findLastElFiber = (fiber: Fiber): Fiber | undefined => {
263 | if (isElement(fiber)) {
264 | return fiber;
265 | } else if (isPortal(fiber)) {
266 | return undefined;
267 | } else {
268 | for (let i = 0; i < (fiber.children?.length ?? 0); i++) {
269 | return findLastElFiber(fiber.children!.at(-(i + 1))!);
270 | }
271 | }
272 | };
273 |
274 | export type ContainerElement = Exclude;
275 |
276 | export const getContainerElFiber = (
277 | fiber: Fiber | undefined
278 | ): Fiber | undefined => {
279 | while ((fiber = fiber?.parent)) {
280 | if (isPortal(fiber) || isElement(fiber)) return fiber;
281 | }
282 | };
283 |
284 | export const findToRoot = (
285 | fiber: Fiber | undefined,
286 | cb: (fiber: Fiber) => boolean
287 | ): Fiber | undefined => {
288 | while ((fiber = fiber?.parent)) {
289 | if (cb(fiber)) return fiber;
290 | }
291 | };
292 |
293 | export const findRoot = (fiber: Fiber) =>
294 | findToRoot(fiber, (fiber) => !fiber.parent)!;
295 |
296 | export const findRuntime = (fiber: Fiber) =>
297 | fiber.reconcileState?.rootWorkingFiber
298 | ? fiber.reconcileState.rootWorkingFiber.runtime!
299 | : findRoot(fiber).runtime!;
300 |
--------------------------------------------------------------------------------
/packages/unis-core/src/h.ts:
--------------------------------------------------------------------------------
1 | import { isNum, isStr, keys, toArray } from "./utils";
2 | import {
3 | COMPONENT,
4 | createFiber,
5 | ELEMENT,
6 | Fiber,
7 | FiberEl,
8 | PORTAL,
9 | TEXT,
10 | } from "./fiber";
11 |
12 | export const h = (tag: any, props: any, ...children: any[]) => {
13 | props = { ...props };
14 | if (children.length === 1) props.children = children[0];
15 | if (children.length > 1) props.children = children;
16 | return createFiber({
17 | tag,
18 | type: isStr(tag) ? ELEMENT : COMPONENT,
19 | props,
20 | ...tag.take,
21 | });
22 | };
23 |
24 | export const h2 = (tag: any, props: any, key?: string | number) => {
25 | if (key !== undefined) props.key = key;
26 | return createFiber({
27 | tag,
28 | type: isStr(tag) ? ELEMENT : COMPONENT,
29 | props,
30 | ...tag.take,
31 | });
32 | };
33 |
34 | export const formatChildren = (children: any) => {
35 | const formatedChildren: Fiber[] = [];
36 |
37 | for (const child of toArray(children)) {
38 | if ([null, false, true, undefined].includes(child)) {
39 | continue;
40 | } else {
41 | Array.isArray(child)
42 | ? formatedChildren.push(...formatChildren(child))
43 | : formatedChildren.push(
44 | isStr(child) || isNum(child)
45 | ? createFiber({
46 | type: TEXT,
47 | props: { nodeValue: child },
48 | })
49 | : child
50 | );
51 | }
52 | }
53 |
54 | return formatedChildren;
55 | };
56 |
57 | export const createRoot = (element: any, container: FiberEl): Fiber => {
58 | return {
59 | tag: (container as any).tagName.toLocaleLowerCase(),
60 | type: ELEMENT,
61 | el: container,
62 | index: 0,
63 | props: {
64 | children: toArray(element),
65 | },
66 | };
67 | };
68 |
69 | export const createPortal = (child: JSX.Element, container: FiberEl) =>
70 | createFiber({
71 | type: PORTAL,
72 | props: { children: child },
73 | to: container,
74 | });
75 |
76 | const defaultCompare = (newProps: any = {}, oldProps: any = {}) => {
77 | const newKeys = keys(newProps);
78 | const oldKeys = keys(oldProps);
79 | if (newKeys.length !== oldKeys.length) return false;
80 | return newKeys.every((key) => Object.is(newProps[key], oldProps[key]));
81 | };
82 |
83 | export const memo = <
84 | T extends ((props: any) => JSX.Element) & { compare?: Function }
85 | >(
86 | child: T,
87 | compare: Function = defaultCompare
88 | ) => {
89 | child.compare = compare;
90 | return child;
91 | };
92 |
93 | export const cloneElement = (
94 | element: Fiber,
95 | props = {},
96 | ...children: JSX.Element[]
97 | ) => h(element.tag, { ...props, ...props }, ...children);
98 |
99 | export const createElement = h;
100 |
101 | export const Fragment = (props: any) => props.children;
102 |
103 | export const FGMT = Fragment;
104 |
--------------------------------------------------------------------------------
/packages/unis-core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./h";
2 | export * from "./fiber";
3 | export * from "./context";
4 | export * from "./reconcile";
5 | export * from "./utils";
6 | export * from "./diff";
7 | export * from "./createTokTik";
8 | export * from "./api/utils";
9 | export * from "./api/use";
10 | export * from "./api/useState";
11 | export * from "./api/useContext";
12 | export * from "./api/useProps";
13 | export * from "./api/useReducer";
14 | export * from "./api/useMemo";
15 | export * from "./api/useEffect";
16 | export * from "./api/useLayoutEffect";
17 | export * from "./api/useId";
18 | export * from "./api/useRef";
19 |
20 | export type * from "../types/jsx";
21 |
22 | export const unisFns = {
23 | use: 1,
24 | useState: 1,
25 | useProps: 1,
26 | useContext: 1,
27 | useReducer: 2,
28 | useMemo: 2,
29 | };
30 |
--------------------------------------------------------------------------------
/packages/unis-core/src/reconcile.ts:
--------------------------------------------------------------------------------
1 | import {
2 | clearAndRunEffects,
3 | clearEffects,
4 | Effect,
5 | EFFECT_TYPE,
6 | effectDepsEqual,
7 | runEffects,
8 | runStateEffects,
9 | } from "./api/utils";
10 | import {
11 | createNext,
12 | Fiber,
13 | FLAG,
14 | ReconcileState,
15 | isComponent,
16 | createFiber,
17 | matchFlag,
18 | findRuntime,
19 | isElement,
20 | } from "./fiber";
21 | import { commit } from "./commit";
22 | import { preElFiberWalkHook } from "./reconcileWalkHooks/preElFiber";
23 | import { effectWalkHook } from "./reconcileWalkHooks/effect";
24 | import { formatChildren } from "./h";
25 | import { isFun } from "./utils";
26 | import { diff } from "./diff";
27 | import { contextWalkHook } from "./reconcileWalkHooks/context";
28 | import { cutMemorizeState } from "./api/useReducer";
29 |
30 | let workingFiber: Fiber | undefined;
31 |
32 | export const getWorkingFiber = () => workingFiber;
33 | export const setWorkingFiber = (fiber: Fiber | undefined) =>
34 | (workingFiber = fiber);
35 |
36 | // reconcile walker
37 | const [next, addHook] = createNext();
38 |
39 | // preEl
40 | addHook(preElFiberWalkHook);
41 | // effect
42 | addHook(effectWalkHook);
43 | // context
44 | addHook(contextWalkHook);
45 |
46 | export const readyForWork = (rootCurrentFiber: Fiber, hydrate = false) => {
47 | rootCurrentFiber.runtime!.toktik.addTok(() =>
48 | performWork(rootCurrentFiber, hydrate)
49 | );
50 | };
51 |
52 | const performWork = (rootCurrentFiber: Fiber, hydrate: boolean) => {
53 | const rootWorkingFiber = createFiber({
54 | index: rootCurrentFiber.index,
55 | tag: rootCurrentFiber.tag,
56 | type: rootCurrentFiber.type,
57 | props: rootCurrentFiber.props,
58 | alternate: rootCurrentFiber,
59 | el: rootCurrentFiber.el,
60 | runtime: rootCurrentFiber.runtime,
61 | });
62 |
63 | const initialReconcileState: ReconcileState = {
64 | rootWorkingFiber,
65 | dispatchEffectList: [],
66 | commitList: [],
67 | dependencyList: [],
68 | workingPreElFiber: undefined,
69 | hydrate,
70 | hydrateEl: rootCurrentFiber.el,
71 | };
72 |
73 | rootWorkingFiber.reconcileState = initialReconcileState;
74 |
75 | setWorkingFiber(rootWorkingFiber);
76 | tickWork(rootWorkingFiber!);
77 | };
78 |
79 | const tickWork = (workingFiber: Fiber) => {
80 | const { toktik } = findRuntime(workingFiber);
81 |
82 | let iFiber: Fiber | undefined = workingFiber;
83 |
84 | // work loop
85 | while (iFiber && !toktik.shouldYield()) {
86 | const isReuse = !!matchFlag(iFiber.commitFlag, FLAG.REUSE);
87 | {
88 | !isReuse && update(iFiber);
89 | !isReuse && isElement(iFiber) && compose(iFiber);
90 | complete(iFiber);
91 | }
92 | iFiber = next(iFiber, isReuse);
93 | setWorkingFiber(iFiber);
94 | }
95 |
96 | if (iFiber) {
97 | toktik.addTik(() => {
98 | setWorkingFiber(iFiber);
99 | tickWork(iFiber!);
100 | });
101 | } else {
102 | const { reconcileState } = workingFiber;
103 |
104 | // switch dispatch bind fiber
105 | runEffects(reconcileState!.dispatchEffectList);
106 |
107 | // commit
108 | commit(reconcileState!);
109 |
110 | // call component effects
111 | callComponentEffects(reconcileState!);
112 |
113 | // clear reconcileState
114 | for (const prop of Object.keys(reconcileState!)) {
115 | delete reconcileState![prop as keyof ReconcileState];
116 | }
117 | }
118 | };
119 |
120 | const callComponentEffects = (reconcileState: ReconcileState) => {
121 | const { commitList, rootWorkingFiber } = reconcileState!;
122 | const { toktik } = rootWorkingFiber.runtime!;
123 |
124 | const triggeredLayoutEffects: Effect[] = [];
125 | const tickEffects: Effect[] = [];
126 |
127 | // clear and run layoutEffects
128 | for (const fiber of commitList) {
129 | if (!isComponent(fiber)) continue;
130 | for (const effect of fiber.effects ?? []) {
131 | if (effect.type === EFFECT_TYPE.TICK) {
132 | tickEffects.push(effect);
133 | } else {
134 | const equal = effectDepsEqual(effect);
135 | if (!equal) {
136 | triggeredLayoutEffects.push(effect);
137 | clearEffects([effect]);
138 | }
139 | }
140 | }
141 | }
142 |
143 | // run triggered layout effects
144 | runEffects(triggeredLayoutEffects);
145 |
146 | // clear and run tick effects
147 | toktik.addTik(() => clearAndRunEffects(tickEffects));
148 | };
149 |
150 | const update = (fiber: Fiber) => {
151 | if (isComponent(fiber)) {
152 | updateComponent(fiber);
153 | } else {
154 | updateHost(fiber);
155 | }
156 | };
157 |
158 | const updateHost = (fiber: Fiber) => {
159 | diff(fiber, fiber.alternate?.children, formatChildren(fiber.props.children));
160 | };
161 |
162 | const updateComponent = (fiber: Fiber) => {
163 | if (!fiber.renderFn) {
164 | fiber.renderFn = fiber.tag as Function;
165 | let rendered = fiber.renderFn(fiber.props);
166 | if (isFun(rendered)) {
167 | fiber.renderFn = rendered;
168 | rendered = fiber.renderFn!();
169 | }
170 | fiber.rendered = formatChildren(rendered);
171 | } else {
172 | runStateEffects(fiber);
173 | if (matchFlag(fiber.commitFlag, FLAG.UPDATE)) {
174 | fiber.rendered = formatChildren(fiber.renderFn(fiber.props));
175 | } else {
176 | /**
177 | * this condition, means `fiber.alternate` is on childFlag marked chain, and `fiber.commitFlag` is undefined.
178 | * diff will keep going on.
179 | */
180 | }
181 | }
182 |
183 | cutMemorizeState(fiber);
184 |
185 | diff(fiber, fiber.alternate?.children, fiber.rendered);
186 | };
187 |
188 | const compose = (fiber: Fiber) => {
189 | const { hydrate, hydrateEl } = fiber.reconcileState!;
190 | const { operator } = findRuntime(fiber);
191 |
192 | if (hydrate && hydrateEl) {
193 | if (!operator.matchElement(fiber, hydrateEl))
194 | throw new Error("Hydrate failed!");
195 | fiber.el = hydrateEl;
196 | fiber.reconcileState!.hydrateEl = operator.nextElement(hydrateEl);
197 | } else if (matchFlag(fiber.commitFlag, FLAG.CREATE) && !hydrate) {
198 | fiber.el = operator.createElement(fiber);
199 | fiber.attrDiff?.length && operator.updateElementProperties(fiber);
200 |
201 | let iFiber: Fiber | undefined = fiber;
202 |
203 | while ((iFiber = iFiber.parent)) {
204 | if (!matchFlag(iFiber.commitFlag, FLAG.CREATE)) break;
205 | if (isElement(iFiber)) {
206 | operator.insertBefore(iFiber, fiber.el, null);
207 | break;
208 | }
209 | }
210 | }
211 | };
212 |
213 | const complete = (fiber: Fiber) => {
214 | !fiber.commitFlag && (fiber.alternate = undefined);
215 | };
216 |
--------------------------------------------------------------------------------
/packages/unis-core/src/reconcileWalkHooks/context.ts:
--------------------------------------------------------------------------------
1 | import { createDependency, findDependency } from "../context";
2 | import { createNext, Fiber, isProvider, WalkHook } from "../fiber";
3 | import { markFiber } from "../api/utils";
4 |
5 | export const contextWalkHook: WalkHook = {
6 | down: (from: Fiber, to?: Fiber) => {
7 | isProvider(from) &&
8 | from.reconcileState!.dependencyList.push(createDependency(from));
9 | },
10 |
11 | up: (from: Fiber, to?: Fiber) => {
12 | to && isProvider(to) && from.reconcileState!.dependencyList.pop();
13 | },
14 |
15 | enter: (enter: Fiber, skipChild: boolean) => {
16 | if (
17 | enter.alternate &&
18 | isProvider(enter.alternate) &&
19 | !Object.is(enter.alternate.props.value, enter.props.value)
20 | ) {
21 | let alternate = enter.alternate;
22 | let iFiber: Fiber | undefined = alternate;
23 |
24 | const [next, addHook] = createNext();
25 |
26 | addHook({ up: (from, to) => to !== alternate });
27 |
28 | do {
29 | findDependency(iFiber, enter) && markFiber(iFiber);
30 | iFiber = next(
31 | iFiber,
32 | iFiber !== alternate && isProvider(iFiber) && iFiber.tag === enter.tag
33 | );
34 | } while (iFiber);
35 | }
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/packages/unis-core/src/reconcileWalkHooks/effect.ts:
--------------------------------------------------------------------------------
1 | import { Fiber, WalkHook } from "../fiber";
2 |
3 | export const pushEffect = (fiber: Fiber) => {
4 | fiber.reconcileState!.commitList.push(fiber);
5 | };
6 |
7 | export const effectWalkHook: WalkHook = {
8 | up: (from, to) => {
9 | !from.child && from.commitFlag && pushEffect(from);
10 | to?.commitFlag && pushEffect(to);
11 | },
12 | sibling: (from) => {
13 | !from.child && from.commitFlag && pushEffect(from);
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/packages/unis-core/src/reconcileWalkHooks/preElFiber.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Fiber,
3 | findLastElFiber,
4 | FLAG,
5 | isElement,
6 | isPortal,
7 | matchFlag,
8 | WalkHook,
9 | } from "../fiber";
10 |
11 | const setWorkingPreElFiber = (
12 | fiber: Fiber,
13 | workingPreElFiber: Fiber | undefined
14 | ) => {
15 | if (fiber.reconcileState)
16 | fiber.reconcileState.workingPreElFiber = workingPreElFiber;
17 | };
18 |
19 | const setReuseFiberPreElFiber = (fiber: Fiber) => {
20 | if (!matchFlag(fiber.commitFlag, FLAG.REUSE)) return;
21 | const lastElFiber = findLastElFiber(fiber.alternate!);
22 | lastElFiber && setWorkingPreElFiber(fiber, lastElFiber);
23 | };
24 |
25 | export const preElFiberWalkHook: WalkHook = {
26 | down: (from: Fiber, to?: Fiber) => {
27 | isElement(from) && setWorkingPreElFiber(from, undefined);
28 | isPortal(from) && setWorkingPreElFiber(from, undefined);
29 | },
30 |
31 | up: (from: Fiber, to?: Fiber) => {
32 | if (from && !from.child) {
33 | isElement(from) && setWorkingPreElFiber(from, from);
34 | }
35 | if (to) {
36 | isElement(to) && setWorkingPreElFiber(from, to);
37 | isPortal(to) && setWorkingPreElFiber(from, to.preElFiber);
38 | }
39 | setReuseFiberPreElFiber(from);
40 | },
41 |
42 | sibling: (from: Fiber, to?: Fiber) => {
43 | if (matchFlag(from.commitFlag, FLAG.REUSE)) {
44 | setReuseFiberPreElFiber(from);
45 | } else {
46 | isElement(from) && setWorkingPreElFiber(from, from);
47 | }
48 | },
49 |
50 | return: (retn?: Fiber) => {
51 | if (retn && matchFlag(retn.commitFlag, FLAG.INSERT))
52 | retn.preElFiber = retn.reconcileState!.workingPreElFiber;
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/packages/unis-core/src/svg.ts:
--------------------------------------------------------------------------------
1 | // kebab svg attr keys
2 | export const displayAttrs = [
3 | "baselineShift",
4 | "alignmentBaseline",
5 | "clip",
6 | "clipPath",
7 | "clipRule",
8 | "color",
9 | "colorInterpolation",
10 | "colorInterpolationFilters",
11 | "colorProfile",
12 | "colorRendering",
13 | "cursor",
14 | "direction",
15 | "display",
16 | "dominantBaseline",
17 | "enableBackground",
18 | "fill",
19 | "fillOpacity",
20 | "fillRule",
21 | "filter",
22 | "floodColor",
23 | "floodOpacity",
24 | "fontFamily",
25 | "fontSize",
26 | "fontSizeAdjust",
27 | "fontStretch",
28 | "fontStyle",
29 | "fontVariant",
30 | "fontWeight",
31 | "glyphOrientationHorizontal",
32 | "glyphOrientationVertical",
33 | "imageRendering",
34 | "kerning",
35 | "letterSpacing",
36 | "lightingColor",
37 | "markerEnd",
38 | "markerMid",
39 | "markerStart",
40 | "mask",
41 | "opacity",
42 | "overflow",
43 | "pointerEvents",
44 | "shapeRendering",
45 | "stopColor",
46 | "stopOpacity",
47 | "stroke",
48 | "strokeDasharray",
49 | "strokeDashoffset",
50 | "strokeLinecap",
51 | "strokeLinejoin",
52 | "strokeMiterlimit",
53 | "strokeOpacity",
54 | "strokeWidth",
55 | "textAnchor",
56 | "transform",
57 | "textDecoration",
58 | "textRendering",
59 | "unicodeBidi",
60 | "vectorEffect",
61 | "visibility",
62 | "wordSpacing",
63 | "writingMode",
64 | ];
65 |
--------------------------------------------------------------------------------
/packages/unis-core/src/utils.ts:
--------------------------------------------------------------------------------
1 | import type { CSArray, CSObject } from "../types/jsx";
2 | import { displayAttrs } from "./svg";
3 |
4 | export const keys = Object.keys;
5 |
6 | export const type = (a: any) =>
7 | Object.prototype.toString.bind(a)().slice(8, -1);
8 |
9 | export const isFun = (a: any): a is Function => typeof a === "function";
10 | export const isStr = (a: any): a is string => typeof a === "string";
11 | export const isNum = (a: any): a is number => typeof a === "number";
12 | export const isBool = (a: any): a is boolean => typeof a === "boolean";
13 | export const isSymbol = (a: any): a is boolean => typeof a === "symbol";
14 |
15 | export const isArray = Array.isArray;
16 | export const isObject = (a: any): a is object => type(a) === "Object";
17 |
18 | export const isNullish = (a: any): a is null | undefined => a == null;
19 |
20 | export const isEvent = (a: string) => a.startsWith("on");
21 | export const getEventName = (event: string) => {
22 | const [, eventName, capture] = event.match(/^on(.*)(Capture)?$/)!;
23 | return [eventName.toLowerCase(), !!capture] as const;
24 | };
25 |
26 | export const camel2kebab = (text: string) =>
27 | text.replace(/([A-Z])/g, "-$1").toLowerCase();
28 |
29 | export const toArray = (a: T) => (Array.isArray(a) ? a : [a]);
30 |
31 | export const arraysEqual = (a: any, b: any) => {
32 | if (a == null || b == null) return false;
33 | if (a.length !== b.length) return false;
34 | for (var i = 0; i < a.length; ++i) {
35 | if (!Object.is(a[i], b[i])) return false;
36 | }
37 | return true;
38 | };
39 |
40 | export const styleStr = (style: Partial) =>
41 | keys(style)
42 | .map(
43 | (key) => `${camel2kebab(key)}: ${style[key as keyof CSSStyleDeclaration]}`
44 | )
45 | .join("; ") + ";";
46 |
47 | export const svgKey = (key: string) => {
48 | for (const str of ["xmlns", "xml", "xlink"]) {
49 | if (key.startsWith(str)) return key.toLowerCase().replace(str, `${str}:`);
50 | }
51 | return displayAttrs.includes(key) ? camel2kebab(key) : key;
52 | };
53 |
54 | export const classes = (cs: CSArray | CSObject): string => {
55 | const objectClasses = (objcs: Record) =>
56 | keys(objcs)
57 | .reduce((pre, cur) => pre + " " + (objcs[cur] ? cur : ""), "")
58 | .trim();
59 |
60 | const arrayClasses = (arrcs: CSArray) =>
61 | arrcs
62 | .reduce(
63 | (pre: string, cur) =>
64 | pre +
65 | " " +
66 | `${
67 | isNum(cur) || isStr(cur)
68 | ? cur
69 | : isObject(cur)
70 | ? objectClasses(cur)
71 | : isArray(cur)
72 | ? classes(cur)
73 | : ""
74 | }`,
75 | ""
76 | )
77 | .trim();
78 |
79 | return isArray(cs) ? arrayClasses(cs) : objectClasses(cs);
80 | };
81 |
82 | let overflow = "";
83 | let count = 0;
84 |
85 | export const generateId = () => {
86 | if (count === Number.MAX_SAFE_INTEGER) {
87 | overflow += count.toString(32);
88 | count = 0;
89 | }
90 | return `${overflow}${(count++).toString(32)}`;
91 | };
92 |
--------------------------------------------------------------------------------
/packages/unis-core/test/utils.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @vitest-environment jsdom
3 | */
4 | import { expect, it } from "vitest";
5 | import { classes, svgKey, styleStr } from "../src/utils";
6 |
7 | it("classes", () => {
8 | expect(classes(["a", "b", 1, ["c", { d: true }]])).toBe("a b 1 c d");
9 | expect(classes({ a: true, b: undefined, c: null })).toBe("a");
10 | expect(classes({ a: false, b: true, c: null })).toBe("b");
11 | });
12 |
13 | it("realSVGAttr", () => {
14 | expect(svgKey("glyphOrientationVertical")).toBe("glyph-orientation-vertical");
15 | });
16 |
17 | it("style2String", () => {
18 | expect(styleStr({ background: "yellow", fontSize: "14px" })).toBe(
19 | "background: yellow; font-size: 14px;"
20 | );
21 | });
22 |
--------------------------------------------------------------------------------
/packages/unis-core/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src"],
4 | "exclude": ["**/*.test.*"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/unis-core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "build"
5 | },
6 | "include": ["src", "test", "jsx-runtime", "types/jsx.d.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/unis-core/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 |
3 | export default defineConfig({
4 | test: {
5 | include: ["**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
6 | coverage: {
7 | reporter: ["text", "json", "html"],
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/packages/unis-dom/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 | .parcel-cache
78 |
79 | # Next.js build output
80 | .next
81 |
82 | # Nuxt.js build / generate output
83 | .nuxt
84 | dist
85 | build
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | .vuepress/dist
95 |
96 | # Serverless directories
97 | .serverless/
98 |
99 | # FuseBox cache
100 | .fusebox/
101 |
102 | # DynamoDB Local files
103 | .dynamodb/
104 |
105 | # TernJS port file
106 | .tern-port
107 |
--------------------------------------------------------------------------------
/packages/unis-dom/index.d.ts:
--------------------------------------------------------------------------------
1 | import "@unis/core";
2 | export * from "./dist/browser";
3 |
--------------------------------------------------------------------------------
/packages/unis-dom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@unis/dom",
3 | "version": "1.2.5",
4 | "description": "Unis is a simpler and easier to use front-end framework than React",
5 | "main": "dist/browser.js",
6 | "module": "dist/browser.mjs",
7 | "types": "index.d.ts",
8 | "typings": "index.d.ts",
9 | "exports": {
10 | ".": {
11 | "require": "./dist/browser.js",
12 | "import": "./dist/browser.mjs"
13 | },
14 | "./server": {
15 | "require": "./dist/server.js",
16 | "import": "./dist/server.mjs"
17 | }
18 | },
19 | "scripts": {
20 | "build": "rimraf build && rimraf dist && tsc -p tsconfig.build.json && rollup --config",
21 | "build:dev": "cross-env NODE_ENV=development pnpm build",
22 | "build:server": "rollup --config rollup.config.server.mjs",
23 | "test": "vitest run --coverage",
24 | "test:watch": "vitest -w"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/anuoua/unis.git"
29 | },
30 | "keywords": [
31 | "frontend",
32 | "web",
33 | "framwork"
34 | ],
35 | "files": [
36 | "dist",
37 | "server.d.ts",
38 | "index.d.ts"
39 | ],
40 | "author": "anuoua",
41 | "license": "MIT",
42 | "bugs": {
43 | "url": "https://github.com/anuoua/unis/issues"
44 | },
45 | "homepage": "https://github.com/anuoua/unis#readme",
46 | "peerDependencies": {
47 | "@unis/core": "workspace:^"
48 | },
49 | "devDependencies": {
50 | "@rollup/plugin-node-resolve": "^15.0.2",
51 | "@types/jsdom": "^21.1.1",
52 | "@unis/core": "workspace:^",
53 | "@unis/vite-preset": "workspace:^",
54 | "@vitest/coverage-c8": "^0.28.5",
55 | "cross-env": "^7.0.3",
56 | "esbuild": "^0.17.15",
57 | "jsdom": "^21.1.1",
58 | "rimraf": "^4.4.1",
59 | "rollup": "^3.20.2",
60 | "rollup-plugin-dts": "^5.3.0",
61 | "rollup-plugin-esbuild": "^5.0.0",
62 | "tslib": "^2.5.0",
63 | "typescript": "^4.9.5",
64 | "vite": "^4.2.1",
65 | "vitest": "^0.29.8"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/unis-dom/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "rollup-plugin-esbuild";
2 | import dts from "rollup-plugin-dts";
3 | import { defineConfig } from "rollup";
4 | import { nodeResolve } from "@rollup/plugin-node-resolve";
5 |
6 | const configGen = (format, plateform) =>
7 | defineConfig({
8 | input: `src/${plateform}/index.ts`,
9 | external: [/^@unis/],
10 | output: [
11 | {
12 | dir: "dist",
13 | entryFileNames: `${plateform}.${format === "esm" ? "mjs" : "js"}`,
14 | format,
15 | sourcemap: true,
16 | },
17 | ],
18 | plugins: [
19 | nodeResolve(),
20 | esbuild({
21 | sourceMap: true,
22 | target: "esnext",
23 | }),
24 | ],
25 | });
26 |
27 | const dtsRollup = (which) =>
28 | defineConfig({
29 | input: `build/${which}/index.d.ts`,
30 | output: [{ file: `dist/${which}.d.ts`, format: "es" }],
31 | plugins: [dts()],
32 | });
33 |
34 | const config = [
35 | configGen("cjs", "browser"),
36 | configGen("esm", "browser"),
37 | configGen("cjs", "server"),
38 | configGen("esm", "server"),
39 | dtsRollup("browser"),
40 | dtsRollup("server"),
41 | ];
42 |
43 | export default config;
44 |
--------------------------------------------------------------------------------
/packages/unis-dom/server.d.ts:
--------------------------------------------------------------------------------
1 | export * from "./dist/server";
2 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/__test__/context.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @vitest-environment jsdom
3 | */
4 | import { afterEach, beforeEach, expect, it } from "vitest";
5 | import { useContext } from "@unis/core";
6 | import { useEffect } from "@unis/core";
7 | import { useProps } from "@unis/core";
8 | import { useState } from "@unis/core";
9 | import { createContext } from "@unis/core";
10 | import { Fragment, memo } from "@unis/core";
11 | import { rendered, testRender } from "./util";
12 |
13 | let root: Element;
14 |
15 | beforeEach(() => {
16 | root = document.createElement("div");
17 | document.body.append(root);
18 | });
19 |
20 | afterEach(() => {
21 | root.innerHTML = "";
22 | });
23 |
24 | it("context", async () => {
25 | const AppContext = createContext("initial");
26 |
27 | const Cpp = memo(() => {
28 | let theme = useContext(AppContext);
29 |
30 | return () => Cpp: {theme}
;
31 | });
32 |
33 | const Dpp = () => {
34 | return () => (
35 |
36 | {(theme) => Dpp: {theme}
}
37 |
38 | );
39 | };
40 |
41 | const Epp = () => {
42 | return () => (
43 |
44 | {(theme) => Epp: {theme}
}
45 |
46 | );
47 | };
48 |
49 | const Bpp = () => {
50 | let theme = useContext(AppContext);
51 |
52 | return () => (
53 |
54 | Bpp: {theme}
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | const App = () => {
63 | let [theme, setTheme] = useState("light");
64 |
65 | useEffect(
66 | () => {
67 | setTheme("dark");
68 | },
69 | () => []
70 | );
71 |
72 | return () => (
73 |
74 |
75 | App
76 |
77 |
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | testRender( , root);
85 |
86 | expect(root.innerHTML).toBe(
87 | "App
Dpp: light
Epp: initial
"
88 | );
89 |
90 | await rendered();
91 |
92 | expect(root.innerHTML).toBe(
93 | "App
Dpp: dark
Epp: initial
"
94 | );
95 | });
96 |
97 | it("context pass through", async () => {
98 | const AppContext = createContext({} as any);
99 |
100 | const App = () => {
101 | let [hello, setHello] = useState("hello");
102 |
103 | return () => (
104 |
105 |
106 |
107 |
108 |
109 | );
110 | };
111 |
112 | const Bpp = () => {
113 | let { hello, setHello } = useContext(AppContext);
114 | return () => ;
115 | };
116 |
117 | const Cpp = (p: { msg: string; setMsg: (msg: string) => void }) => {
118 | let { msg, setMsg } = useProps(p);
119 | let [count, setCount] = useState(0);
120 |
121 | useEffect(
122 | () => {
123 | setMsg("world");
124 | setCount(count + 1);
125 | },
126 | () => []
127 | );
128 |
129 | return () => msg;
130 | };
131 |
132 | testRender( , root);
133 |
134 | expect(root.innerHTML).toBe("hello
");
135 |
136 | await rendered();
137 |
138 | expect(root.innerHTML).toBe("world
");
139 | });
140 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/__test__/dom.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @vitest-environment jsdom
3 | */
4 | import { afterEach, beforeEach, expect, it } from "vitest";
5 | import { useEffect } from "@unis/core";
6 | import { useState } from "@unis/core";
7 | import { rendered, testRender } from "./util";
8 |
9 | let root: Element;
10 |
11 | beforeEach(() => {
12 | root = document.createElement("div");
13 | document.body.append(root);
14 | });
15 |
16 | afterEach(() => {
17 | root.innerHTML = "";
18 | });
19 |
20 | it("dom", async () => {
21 | const App = () => {
22 | let [toggle, setToggle] = useState(true);
23 |
24 | const getCurrentStyle = () => {
25 | return toggle
26 | ? {
27 | style: {
28 | background: "yellow",
29 | },
30 | tabindex: "1",
31 | className: "class1",
32 | onClick: () => {},
33 | }
34 | : {
35 | style: {
36 | background: "red",
37 | },
38 | tabindex: "2",
39 | onClick: () => {},
40 | };
41 | };
42 |
43 | useEffect(
44 | () => {
45 | setToggle(false);
46 | },
47 | () => []
48 | );
49 |
50 | return () => {
51 | return hello
;
52 | };
53 | };
54 |
55 | testRender( , root);
56 | expect(root.innerHTML).toBe(
57 | 'hello
'
58 | );
59 | await rendered();
60 | expect(root.innerHTML).toBe(
61 | 'hello
'
62 | );
63 | });
64 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/__test__/effect.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @vitest-environment jsdom
3 | */
4 | import { afterEach, beforeEach, expect, it } from "vitest";
5 | import { useEffect } from "@unis/core";
6 | import { useState } from "@unis/core";
7 | import { rendered, testRender } from "./util";
8 |
9 | let root: Element;
10 |
11 | beforeEach(() => {
12 | root = document.createElement("div");
13 | document.body.append(root);
14 | });
15 |
16 | afterEach(() => {
17 | root.innerHTML = "";
18 | });
19 |
20 | it("effect", async () => {
21 | const Bpp = () => {
22 | useEffect(
23 | () => {
24 | return () => {};
25 | },
26 | () => []
27 | );
28 | return () => "bpp";
29 | };
30 |
31 | const App = () => {
32 | let [visible, setVisible] = useState(true);
33 |
34 | useEffect(
35 | () => {
36 | setVisible(false);
37 | },
38 | () => []
39 | );
40 |
41 | return () => (visible ? : null);
42 | };
43 |
44 | testRender( , root);
45 | expect(root.innerHTML).toBe("bpp");
46 | await rendered();
47 | expect(root.innerHTML).toBe("");
48 | });
49 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/__test__/hydrate.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @vitest-environment jsdom
3 | */
4 | import { afterEach, beforeEach, expect, it } from "vitest";
5 | import { rendered, testRender } from "./util";
6 | import { use, useState } from "@unis/core";
7 |
8 | let root: Element;
9 |
10 | beforeEach(() => {
11 | root = document.createElement("div");
12 | document.body.append(root);
13 | });
14 |
15 | afterEach(() => {
16 | root.innerHTML = "";
17 | });
18 |
19 | it("hydrate", async () => {
20 | root.innerHTML = "Apphello
";
21 |
22 | let setMsgOutter: any;
23 |
24 | const App = () => {
25 | let [msg, setMsg] = useState("hello");
26 |
27 | use(() => {
28 | setMsgOutter = setMsg;
29 | });
30 |
31 | return () => (
32 |
33 | App{msg}
34 |
35 | );
36 | };
37 |
38 | testRender( , root, true);
39 |
40 | expect(root.innerHTML).toBe("Apphello
");
41 |
42 | setMsgOutter("world");
43 |
44 | await rendered();
45 |
46 | expect(root.innerHTML).toBe("Appworld
");
47 | });
48 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/__test__/memo.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @vitest-environment jsdom
3 | */
4 | import { afterEach, beforeEach, expect, it } from "vitest";
5 | import { useEffect } from "@unis/core";
6 | import { useState } from "@unis/core";
7 | import { h2, memo } from "@unis/core";
8 | import { rendered, testRender } from "./util";
9 |
10 | let root: Element;
11 |
12 | beforeEach(() => {
13 | root = document.createElement("div");
14 | document.body.append(root);
15 | });
16 |
17 | afterEach(() => {
18 | root.innerHTML = "";
19 | });
20 |
21 | it("memo", async () => {
22 | const Bpp = memo(() => {
23 | let renderCount = 0;
24 |
25 | return () => {
26 | return {renderCount++}
;
27 | };
28 | });
29 | const App = () => {
30 | let [msg, setMsg] = useState("hello");
31 |
32 | useEffect(
33 | () => {
34 | setMsg("hello world");
35 | },
36 | () => []
37 | );
38 |
39 | return () => (
40 |
41 | {msg}
42 | {h2(Bpp, {}, "key")}
43 |
44 | );
45 | };
46 |
47 | testRender( , root);
48 | expect(root.innerHTML).toBe("");
49 | await rendered();
50 | expect(root.innerHTML).toBe("");
51 | });
52 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/__test__/portal.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @vitest-environment jsdom
3 | */
4 | import { afterEach, beforeEach, expect, it } from "vitest";
5 | import { useEffect } from "@unis/core";
6 | import { useState } from "@unis/core";
7 | import { createPortal, Fragment } from "@unis/core";
8 | import { rendered, testRender } from "./util";
9 |
10 | let root: Element;
11 | let dialog: Element;
12 |
13 | beforeEach(() => {
14 | root = document.createElement("div");
15 | dialog = document.createElement("div");
16 | document.body.append(root, dialog);
17 | });
18 |
19 | afterEach(() => {
20 | root.innerHTML = "";
21 | dialog.innerHTML = "";
22 | });
23 |
24 | it("portal", async () => {
25 | const App = () => {
26 | let [visible, setVisible] = useState(true);
27 |
28 | useEffect(
29 | () => {
30 | setVisible(false);
31 | },
32 | () => []
33 | );
34 |
35 | return () => (
36 |
37 | hello
38 | {visible && createPortal(hello dialog , dialog)}
39 |
40 | );
41 | };
42 |
43 | testRender( , root);
44 | expect(document.body.innerHTML).toBe(
45 | "hello dialog
"
46 | );
47 | await rendered();
48 | expect(document.body.innerHTML).toBe(
49 | "
"
50 | );
51 | });
52 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/__test__/reconcile.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @vitest-environment jsdom
3 | */
4 | import { afterEach, beforeEach, expect, it } from "vitest";
5 | import { useEffect } from "@unis/core";
6 | import { useState } from "@unis/core";
7 | import { rendered, testRender } from "./util";
8 |
9 | let root: Element;
10 |
11 | beforeEach(() => {
12 | root = document.createElement("div");
13 | document.body.append(root);
14 | });
15 |
16 | afterEach(() => {
17 | root.innerHTML = "";
18 | });
19 |
20 | it("diff with key", async () => {
21 | const App = () => {
22 | let [toggle, setToggle] = useState(false);
23 |
24 | useEffect(
25 | () => {
26 | setToggle(true);
27 | },
28 | () => []
29 | );
30 |
31 | return () =>
32 | !toggle ? (
33 |
34 |
1
35 |
2
36 |
3
37 |
del
38 |
4
39 |
5
40 |
6
41 |
42 | ) : (
43 |
44 |
1
45 |
5
46 |
4
47 |
2
48 |
3
49 |
6
50 |
51 | );
52 | };
53 |
54 | testRender( , root);
55 | expect(root.innerHTML).toBe(
56 | ''
57 | );
58 | await rendered();
59 | expect(root.innerHTML).toBe(
60 | ''
61 | );
62 | });
63 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/__test__/util.ts:
--------------------------------------------------------------------------------
1 | import { readyForWork, createRoot, createTokTik } from "@unis/core";
2 | import { createOperator } from "../operator";
3 |
4 | const toktik = createTokTik({
5 | nextTick: (cb: VoidFunction) =>
6 | Promise.resolve()
7 | .catch((err) => console.error(err))
8 | .then(() => cb()),
9 | now: () => 0,
10 | });
11 | const operator = createOperator();
12 |
13 | export const testRender = (
14 | element: any,
15 | container: Element,
16 | hydrate = false
17 | ) => {
18 | const rootFiber = createRoot(element, container);
19 | rootFiber.runtime = {
20 | toktik,
21 | operator,
22 | };
23 | readyForWork(rootFiber, hydrate);
24 | };
25 |
26 | export const rendered = () =>
27 | new Promise((resolve) => {
28 | setTimeout(resolve, 0);
29 | });
30 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/__test__/utils.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @vitest-environment jsdom
3 | */
4 | import { expect, it } from "vitest";
5 | import { classes, svgKey, styleStr } from "@unis/core";
6 |
7 | it("classes", () => {
8 | expect(classes(["a", "b", 1, ["c", { d: true }]])).toBe("a b 1 c d");
9 | expect(classes({ a: true, b: undefined, c: null })).toBe("a");
10 | expect(classes({ a: false, b: true, c: null })).toBe("b");
11 | });
12 |
13 | it("realSVGAttr", () => {
14 | expect(svgKey("glyphOrientationVertical")).toBe("glyph-orientation-vertical");
15 | });
16 |
17 | it("style2String", () => {
18 | expect(styleStr({ background: "yellow", fontSize: "14px" })).toBe(
19 | "background: yellow; font-size: 14px;"
20 | );
21 | });
22 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/const.ts:
--------------------------------------------------------------------------------
1 | export const UNIS_ROOT = Symbol("unis_root");
2 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/index.ts:
--------------------------------------------------------------------------------
1 | export { UNIS_ROOT } from "./const";
2 | export { render } from "./render";
3 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/operator.ts:
--------------------------------------------------------------------------------
1 | import { Fiber, findEls, isPortal, isText, Operator } from "@unis/core";
2 | import { getEventName, isEvent, isNullish } from "@unis/core";
3 | import { UNIS_ROOT } from "./const";
4 |
5 | type FiberDomEl = Element | DocumentFragment | SVGAElement | Text | ParentNode;
6 |
7 | interface FiberDom extends Fiber {
8 | el?: FiberDomEl;
9 | }
10 |
11 | export const createOperator = (): Operator => {
12 | const createElement = (fiber: FiberDom) => {
13 | const { tag: type, isSvg } = fiber;
14 | return isText(fiber)
15 | ? document.createTextNode(fiber.props.nodeValue + "")
16 | : isSvg
17 | ? document.createElementNS("http://www.w3.org/2000/svg", type as string)
18 | : document.createElement(type as string);
19 | };
20 |
21 | const nextElement = (el: FiberDomEl | null) => {
22 | while (el) {
23 | if (el.firstChild) return el.firstChild;
24 | if (el.nextSibling) return el.nextSibling;
25 | while ((el = el.parentNode)) {
26 | if ((el as any)[UNIS_ROOT]) return null;
27 | if (el.nextSibling) return el.nextSibling;
28 | }
29 | }
30 | return null;
31 | };
32 |
33 | const matchElement = (fiber: FiberDom, el: Element | Text) =>
34 | el.nodeType === Node.TEXT_NODE
35 | ? isText(fiber)
36 | : (el as Element).tagName.toLocaleLowerCase() === fiber.tag;
37 |
38 | const insertBefore = (
39 | containerFiber: FiberDom,
40 | insertElement: FiberDomEl,
41 | targetElement: FiberDomEl | null
42 | ) => {
43 | (
44 | (isPortal(containerFiber)
45 | ? containerFiber.to
46 | : containerFiber.el)! as FiberDomEl
47 | ).insertBefore(insertElement, targetElement);
48 | };
49 |
50 | const nextSibling = (fiber: FiberDom) => fiber.el!.nextSibling;
51 |
52 | const firstChild = (fiber: FiberDom) => fiber.el!.firstChild;
53 |
54 | const remove = (fiber: FiberDom) => {
55 | const [first, ...rest] = findEls(fiber) as FiberDomEl[];
56 | const parentNode = first?.parentNode;
57 | if (parentNode) {
58 | for (const el of [first, ...rest]) {
59 | parentNode.removeChild(el);
60 | }
61 | }
62 | };
63 |
64 | const updateTextProperties = (fiber: FiberDom) => {
65 | (fiber.el! as Text).nodeValue = fiber.props.nodeValue + "";
66 | };
67 |
68 | const setAttr = (
69 | el: SVGAElement | HTMLElement,
70 | isSvg: boolean,
71 | key: string,
72 | value: string
73 | ) =>
74 | isSvg
75 | ? (el as SVGAElement).setAttributeNS(null, key, value)
76 | : (el as HTMLElement).setAttribute(key, value);
77 |
78 | const removeAttr = (
79 | el: SVGAElement | HTMLElement,
80 | isSvg: boolean,
81 | key: string
82 | ) =>
83 | isSvg
84 | ? (el as SVGAElement).removeAttributeNS(null, key)
85 | : (el as HTMLElement).removeAttribute(key);
86 |
87 | const updateElementProperties = (fiber: FiberDom) => {
88 | let { el, isSvg, attrDiff } = fiber;
89 |
90 | for (const [key, newValue, oldValue] of attrDiff || []) {
91 | const newExist = !isNullish(newValue);
92 | const oldExist = !isNullish(oldValue);
93 | if (key === "ref") {
94 | oldExist && (oldValue.current = undefined);
95 | newExist && (newValue.current = el);
96 | } else if (isEvent(key)) {
97 | const [eventName, capture] = getEventName(key);
98 | oldExist && el!.removeEventListener(eventName, oldValue);
99 | newExist && el!.addEventListener(eventName, newValue, capture);
100 | } else {
101 | newExist
102 | ? setAttr(el as SVGAElement | HTMLElement, isSvg!, key, newValue)
103 | : removeAttr(el as SVGAElement | HTMLElement, isSvg!, key);
104 | }
105 | }
106 | };
107 |
108 | return {
109 | createElement,
110 | nextElement,
111 | matchElement,
112 | insertBefore,
113 | nextSibling,
114 | firstChild,
115 | remove,
116 | updateTextProperties,
117 | updateElementProperties,
118 | };
119 | };
120 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/render.ts:
--------------------------------------------------------------------------------
1 | import { readyForWork, createRoot, createTokTik } from "@unis/core";
2 | import { createOperator } from "./operator";
3 | import { UNIS_ROOT } from "./const";
4 | import { nextTick, now } from "./toktik";
5 |
6 | const operator = createOperator();
7 | const toktik = createTokTik({
8 | now,
9 | nextTick,
10 | interval: (window as any).UNIS_INTERVAL,
11 | });
12 |
13 | export const render = (element: any, container: Element, hydrate = false) => {
14 | (container as any)[UNIS_ROOT] = true;
15 | const rootFiber = createRoot(element, container);
16 | rootFiber.runtime = {
17 | toktik,
18 | operator,
19 | };
20 | readyForWork(rootFiber, hydrate);
21 | };
22 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/browser/toktik.ts:
--------------------------------------------------------------------------------
1 | export const nextTick = (cb: VoidFunction, pending = false) => {
2 | if (pending) {
3 | queueMicrotask(cb);
4 | } else if (window.MessageChannel) {
5 | const { port1, port2 } = new window.MessageChannel();
6 | port1.postMessage("");
7 | port2.onmessage = () => cb();
8 | } else {
9 | setTimeout(() => cb());
10 | }
11 | };
12 |
13 | export const now = () => performance.now();
14 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/server/__test__/server.test.tsx:
--------------------------------------------------------------------------------
1 | import { expect, it } from "vitest";
2 | import { renderToString } from "..";
3 |
4 | it("render to string", () => {
5 | const App = () => {
6 | return () => (
7 |
8 | <>
9 |
hello
10 | world
11 | >
12 |
13 | );
14 | };
15 |
16 | const result = renderToString( );
17 |
18 | expect(result).toBe(
19 | '
hello world '
20 | );
21 | });
22 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/server/index.ts:
--------------------------------------------------------------------------------
1 | import { createRoot, createTokTik, readyForWork } from "@unis/core";
2 | import { createOperator, ElementNode } from "./operator";
3 |
4 | const operator = createOperator();
5 |
6 | const toktik = createTokTik({
7 | nextTick: (cb: VoidFunction) =>
8 | Promise.resolve()
9 | .catch((err) => console.error(err))
10 | .then(() => cb()),
11 | now: () => 0,
12 | });
13 |
14 | export const renderToString = (element: any) => {
15 | const rootNode = new ElementNode("");
16 | const rootFiber = createRoot(element, rootNode);
17 | rootFiber.runtime = {
18 | toktik,
19 | operator,
20 | };
21 | readyForWork(rootFiber);
22 | return rootNode.renderToString();
23 | };
24 |
--------------------------------------------------------------------------------
/packages/unis-dom/src/server/operator.ts:
--------------------------------------------------------------------------------
1 | import { isEvent, isNullish, Operator, Fiber, isText } from "@unis/core";
2 |
3 | export class ElementNode {
4 | children: ServerNode[] = [];
5 |
6 | properties: Record = {};
7 |
8 | constructor(public tagName: string) {}
9 |
10 | insertBefore(
11 | node: ElementNode | TextNode,
12 | child: ElementNode | TextNode | null
13 | ) {
14 | this.children.push(node);
15 | }
16 |
17 | append(...nodes: ServerNode[]) {
18 | this.children.push(...nodes);
19 | }
20 |
21 | renderToString(): string {
22 | const children = this.children
23 | .map((child) => child.renderToString())
24 | .join("");
25 |
26 | const propertiesStr = Object.keys(this.properties)
27 | .map((key) => `${key}="${this.properties[key]}"`)
28 | .join(" ");
29 |
30 | const gap = propertiesStr ? " " : "";
31 |
32 | return this.tagName
33 | ? `<${this.tagName}${gap}${propertiesStr}>${children}${this.tagName}>`
34 | : children;
35 | }
36 | }
37 |
38 | export class TextNode {
39 | constructor(public nodeValue: string) {}
40 | renderToString() {
41 | return this.nodeValue;
42 | }
43 | }
44 |
45 | export type ServerNode = ElementNode | TextNode;
46 |
47 | interface FiberDomServer extends Fiber {
48 | el?: ServerNode;
49 | }
50 |
51 | export const createOperator = (): Operator => {
52 | const createElement = (fiber: FiberDomServer) => {
53 | const { tag: type } = fiber;
54 | return isText(fiber)
55 | ? new TextNode(fiber.props.nodeValue + "")
56 | : new ElementNode(type as string);
57 | };
58 |
59 | const insertBefore = (
60 | containerFiber: FiberDomServer,
61 | insertElement: ServerNode,
62 | targetElement: ServerNode
63 | ) => {
64 | (containerFiber.el as ElementNode).insertBefore(
65 | insertElement,
66 | targetElement
67 | );
68 | };
69 |
70 | const firstChild = (fiber: FiberDomServer) =>
71 | (fiber.el as ElementNode).children[0] ?? null;
72 |
73 | const updateTextProperties = (fiber: FiberDomServer) => {
74 | (fiber.el as TextNode).nodeValue = fiber.props.nodeValue + "";
75 | };
76 |
77 | const updateElementProperties = (fiber: FiberDomServer) => {
78 | let { el, attrDiff } = fiber;
79 |
80 | for (const [key, newValue, oldValue] of attrDiff || []) {
81 | const newExist = !isNullish(newValue);
82 | const oldExist = !isNullish(oldValue);
83 | if (key === "ref") {
84 | oldExist && (oldValue.current = undefined);
85 | newExist && (newValue.current = el);
86 | } else if (isEvent(key)) {
87 | // nothing...
88 | } else {
89 | newExist
90 | ? ((el as ElementNode).properties[key] = newValue)
91 | : delete (el as ElementNode).properties[key];
92 | }
93 | }
94 | };
95 |
96 | return {
97 | nextElement() {},
98 | matchElement: () => false,
99 | remove() {},
100 | nextSibling() {},
101 | createElement,
102 | insertBefore,
103 | firstChild,
104 | updateTextProperties,
105 | updateElementProperties,
106 | };
107 | };
108 |
--------------------------------------------------------------------------------
/packages/unis-dom/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["**/*.test.*", "**/__test__"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/unis-dom/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "build"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/unis-dom/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 | import { unisPreset } from "@unis/vite-preset";
3 |
4 | export default defineConfig({
5 | plugins: [unisPreset()],
6 | test: {
7 | include: ["**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
8 | coverage: {
9 | reporter: ["text", "json", "html"],
10 | },
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/packages/unis-example/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 | .parcel-cache
78 |
79 | # Next.js build output
80 | .next
81 |
82 | # Nuxt.js build / generate output
83 | .nuxt
84 | dist
85 | build
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | .vuepress/dist
95 |
96 | # Serverless directories
97 | .serverless/
98 |
99 | # FuseBox cache
100 | .fusebox/
101 |
102 | # DynamoDB Local files
103 | .dynamodb/
104 |
105 | # TernJS port file
106 | .tern-port
107 |
108 | .DS_Store
109 |
--------------------------------------------------------------------------------
/packages/unis-example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Unis
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/packages/unis-example/other.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.css";
2 |
--------------------------------------------------------------------------------
/packages/unis-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@unis/unis-example",
3 | "version": "0.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "private": "true",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "vite build"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/anuoua/unis.git"
14 | },
15 | "author": "anuoua",
16 | "license": "MIT",
17 | "bugs": {
18 | "url": "https://github.com/anuoua/unis/issues"
19 | },
20 | "homepage": "https://github.com/anuoua/unis#readme",
21 | "dependencies": {
22 | "@unis/core": "workspace:^",
23 | "@unis/dom": "workspace:^",
24 | "@unis/router": "workspace:^",
25 | "@unis/transition": "workspace:^"
26 | },
27 | "devDependencies": {
28 | "@types/lodash": "^4.14.182",
29 | "@unis/vite-preset": "workspace:^",
30 | "autoprefixer": "^10.4.0",
31 | "postcss": "^8.3.11",
32 | "rollup-plugin-reassign": "^1.0.2",
33 | "tailwindcss": "^3.3.1",
34 | "vite": "^4.1.2"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/unis-example/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/packages/unis-example/src/Dialog.tsx:
--------------------------------------------------------------------------------
1 | import { createPortal, useProps, useRef } from "@unis/core";
2 | import { Item } from "./TodoItem";
3 |
4 | interface DialogProps {
5 | onClose: () => void;
6 | onConfirm: (item: Item) => void;
7 | item: Item | null;
8 | }
9 |
10 | export function Dialog(props: DialogProps) {
11 | let { item, onClose, onConfirm } = useProps(props);
12 |
13 | const portalRef = useRef();
14 |
15 | const handleClose = (e: MouseEvent) => {
16 | if ((e.target as HTMLElement) === portalRef.current) {
17 | onClose();
18 | }
19 | };
20 |
21 | const handleConfirm = () => {
22 | onConfirm(item!);
23 | };
24 |
25 | return () =>
26 | createPortal(
27 |
32 |
33 |
34 |
props.onClose()}>
35 |
42 |
48 |
49 |
50 |
51 |
52 |
59 |
65 |
66 |
67 | {" "}
68 | Delete {item?.name} ?
69 |
70 |
71 |
72 |
76 | Confirm
77 |
78 |
79 |
80 |
,
81 | document.querySelector("#dialog")!
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/packages/unis-example/src/Todo.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "@unis/core";
2 | import { CSSTransition, TransitionGroup } from "@unis/transition";
3 | import { Dialog } from "./Dialog";
4 | import { Item, TodoItem } from "./TodoItem";
5 |
6 | let count = 0;
7 |
8 | let todos: any[] = [];
9 |
10 | for (; count < 0; count++) {
11 | todos.push({
12 | id: count,
13 | name: "",
14 | editing: false,
15 | canceled: false,
16 | });
17 | }
18 |
19 | export function ToDo() {
20 | let [todoList, setTodoList] = useState(todos);
21 | let [dialogVisible, setDialogVisible] = useState(false);
22 | let [titleVisible, setTitleVisible] = useState(true);
23 | let [currentItem, setCurrentItem] = useState- (null);
24 |
25 | const handleToggleTitle = () => {
26 | setTitleVisible(!titleVisible);
27 | setTimeout(() => {
28 | setTitleVisible(true);
29 | }, 200);
30 | };
31 |
32 | const handleAdd = (e: any) => {
33 | if (e.key !== "Enter") return;
34 | setTodoList([
35 | ...todoList,
36 | {
37 | id: ++count,
38 | name: e.target.value,
39 | editing: false,
40 | canceled: false,
41 | },
42 | ]);
43 | e.target.value = "";
44 | };
45 |
46 | const handleClose = () => {
47 | setDialogVisible(false);
48 | };
49 |
50 | const handleConfirm = (item: Item) => {
51 | setDialogVisible(false);
52 | setTodoList(todoList.filter((i) => i !== item));
53 | };
54 |
55 | const handleDelete = (item: Item) => {
56 | setCurrentItem(item);
57 | setTodoList(todoList.filter((i) => i !== item));
58 | // setDialogVisible(true);
59 | };
60 |
61 | return () => (
62 |
63 |
64 |
68 | TODO
69 |
70 |
71 |
77 |
78 |
79 | {todoList.map((i: any) => (
80 |
81 |
82 |
83 | ))}
84 |
85 |
86 | {dialogVisible && (
87 |
92 | )}
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/packages/unis-example/src/TodoItem/index.module.css:
--------------------------------------------------------------------------------
1 | .deleteIcon {
2 | color: white;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/unis-example/src/TodoItem/index.tsx:
--------------------------------------------------------------------------------
1 | import { use, memo, useEffect, useProps, useRef } from "@unis/core";
2 | import { Update } from "../hooks/update";
3 | import s from "./index.module.css";
4 |
5 | export interface Item {
6 | id: number;
7 | name: string;
8 | editing: boolean;
9 | canceled: boolean;
10 | }
11 |
12 | interface TodoItemProps {
13 | item: Item;
14 | onDelete: (item: Item) => any;
15 | }
16 |
17 | export const TodoItem = memo((props: TodoItemProps) => {
18 | let { item, onDelete } = useProps(props);
19 | let [render] = use(Update());
20 | let { editing, canceled, name } = use(() => item);
21 |
22 | const inputRef = useRef();
23 |
24 | const handleEditing = () => {
25 | if (canceled) return;
26 | item.editing = true;
27 | render();
28 | };
29 |
30 | const handleClick = () => {
31 | onDelete(item);
32 | };
33 |
34 | const handleCancel = () => {
35 | console.log(inputRef.current);
36 | item.canceled = !item.canceled;
37 | render();
38 | };
39 |
40 | const handleKeyDown = (e: any) => {
41 | if (e.key !== "Enter") return;
42 | item.name = e.target.value;
43 | item.editing = false;
44 | render();
45 | };
46 |
47 | const handleBlur = () => {
48 | item.editing = false;
49 | render();
50 | };
51 |
52 | useEffect(
53 | () => {
54 | item.name = item.name + "x";
55 | render();
56 | },
57 | () => [item.canceled]
58 | );
59 |
60 | return () => {
61 | // console.log("TodoItem render");
62 | return (
63 |
64 | {editing ? (
65 |
73 | ) : canceled ? (
74 |
78 | {name}
79 |
80 | ) : (
81 |
85 | {name}
86 |
87 | )}
88 |
93 | {canceled ? (
94 |
101 |
107 |
108 | ) : (
109 |
116 |
122 |
123 | )}
124 |
125 |
130 |
137 |
143 |
144 |
145 |
146 | );
147 | };
148 | });
149 |
--------------------------------------------------------------------------------
/packages/unis-example/src/Welcome/index.module.css:
--------------------------------------------------------------------------------
1 | .msg {
2 | font-size: 50px;
3 | font-weight: bold;
4 | background: -webkit-linear-gradient(0deg,#e339bc 25%,#4f5b94);
5 | background-clip: text;
6 | -webkit-background-clip: text;
7 | -webkit-text-fill-color: transparent;
8 | transition: all .3s ease;
9 | }
10 |
11 | .msg:hover {
12 | cursor: pointer;
13 | transform: scale(1.2);
14 | background: -webkit-linear-gradient(0deg,#4f5b94 25%,#e339bc);
15 | background-clip: text;
16 | -webkit-background-clip: text;
17 | -webkit-text-fill-color: transparent;
18 | }
--------------------------------------------------------------------------------
/packages/unis-example/src/Welcome/index.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@unis/router";
2 | import s from "./index.module.css";
3 |
4 | export const Welcome = () => {
5 | return () => (
6 |
7 | Unis Todo
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/packages/unis-example/src/bg.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anuoua/unis/a40e220ce9e68377e83a8beb7b7f54782bf3e8ea/packages/unis-example/src/bg.jpeg
--------------------------------------------------------------------------------
/packages/unis-example/src/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | #root {
8 | height: 100%;
9 | }
10 |
11 | .scale-appear {
12 | transform: scale(0) translateZ(0);
13 | }
14 |
15 | .scale-appear-active {
16 | transform: scale(1) translateZ(0);
17 | transition: all 0.4s ease;
18 | }
19 |
20 | .scale-appear-done {
21 | transform: scale(1) translateZ(0);
22 | }
23 |
24 | .scale-enter {
25 | transform: scale(0) translateZ(0);
26 | }
27 |
28 | .scale-enter-active {
29 | transform: scale(1) translateZ(0);
30 | transition: all 0.4s ease;
31 | }
32 |
33 | .scale-enter-done {
34 | transform: scale(1) translateZ(0);
35 | }
36 |
37 | .scale-exit {
38 | transform: scale(1) translateZ(0);
39 | }
40 |
41 | .scale-exit-active {
42 | transform: scale(0) translateZ(0);
43 | transition: all 0.4s ease;
44 | }
45 |
46 | .scale-exit-done {
47 | transform: scale(0) translateZ(0);
48 | }
49 |
50 | .fade-enter {
51 | opacity: 0;
52 | }
53 |
54 | .fade-enter-active {
55 | opacity: 1;
56 | transition: all 0.4s ease;
57 | }
58 |
59 | .fade-enter-done {
60 | opacity: 1;
61 | }
62 |
63 | .fade-exit {
64 | opacity: 1;
65 | }
66 |
67 | .fade-exit-active {
68 | opacity: 0;
69 | transition: all 0.4s ease;
70 | }
71 |
72 | .fade-exit-done {
73 | opacity: 0;
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/packages/unis-example/src/hooks/update.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "@unis/core";
2 |
3 | export const Update = () => {
4 | let [count, setCount] = useState(1);
5 | const update = () => setCount(++count);
6 | return () => [update];
7 | };
8 |
--------------------------------------------------------------------------------
/packages/unis-example/src/index.module.css:
--------------------------------------------------------------------------------
1 | .background_img {
2 | background: url(./bg.jpeg);
3 | z-index: -1;
4 | background-size: cover;
5 | background-position: center;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/unis-example/src/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Fragment,
3 | // useProps,
4 | // useState,
5 | // useEffect
6 | } from "@unis/core";
7 | import { render } from "@unis/dom";
8 | import { ToDo } from "./Todo";
9 | import "./global.css";
10 | import s from "./index.module.css";
11 | import { BrowserRouter, Redirect, Outlet, Route, Routes } from "@unis/router";
12 | import { Welcome } from "./Welcome";
13 |
14 | // const Bpp = (props: { time: number; msg: string }) => {
15 | // let { time, msg } = useProps(props);
16 | // let [visible, setVisible] = useState(false);
17 |
18 | // useEffect(
19 | // () => {
20 | // setVisible(true);
21 | // setTimeout(() => {
22 | // console.log("timeout");
23 | // setVisible(false);
24 | // }, 0);
25 | // },
26 | // () => []
27 | // );
28 |
29 | // setTimeout(() => {
30 | // setVisible(true);
31 | // }, time);
32 |
33 | // return () => (visible ? {msg}
: false);
34 | // };
35 |
36 | const App = () => {
37 | return () => (
38 |
39 |
40 | <>
41 | {/* */}
42 | {/* */}
43 | >
44 | {/* */}
45 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | render(
55 |
56 | }>
57 | } />
58 | } />
59 | } />
60 |
61 | ,
62 | document.querySelector("#root")!
63 | );
64 |
--------------------------------------------------------------------------------
/packages/unis-example/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["./public/**/*.html", "./src/**/*.{js,jsx,ts,tsx,vue}"],
3 | theme: {
4 | extend: {},
5 | },
6 | variants: {
7 | extend: {},
8 | },
9 | plugins: [],
10 | };
11 |
--------------------------------------------------------------------------------
/packages/unis-example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "include": ["src", "other.d.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/unis-example/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import { unisPreset } from "@unis/vite-preset";
3 |
4 | export default defineConfig({
5 | plugins: [unisPreset()],
6 | });
7 |
--------------------------------------------------------------------------------
/packages/unis-router/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | coverage/
3 | dist/
--------------------------------------------------------------------------------
/packages/unis-router/README.md:
--------------------------------------------------------------------------------
1 | # Unis Router
2 |
3 | Router for unis, inspire by [React Router V6](https://github.com/remix-run/react-router).
4 |
5 | ## Install
6 |
7 | ```shell
8 | npm i @unis/router
9 | ```
10 |
11 | ## Usage
12 |
13 | Unis router's api is partial same as React Router V6.
14 |
15 | example
16 |
17 | ```javascript
18 | import { BrowserRouter, Routes, Route, Outlet } from '@unis/router'
19 |
20 | const Dashboard = () => {
21 |
22 | return () => (
23 |
24 | dashboard
25 | // hello
26 |
27 | )
28 | }
29 |
30 | const App = () => {
31 |
32 | return () => (
33 |
34 |
35 | }>
36 | hello
} />
37 |
38 |
39 | )
40 | }
41 |
42 | render(
43 |
44 |
45 | , document.querySelector('#app'))
46 | ```
47 |
48 |
49 |
--------------------------------------------------------------------------------
/packages/unis-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@unis/router",
3 | "version": "0.1.0",
4 | "description": "Unis router component",
5 | "main": "dist/index.js",
6 | "module": "dist/index.mjs",
7 | "types": "dist/index.d.ts",
8 | "typings": "dist/index.d.ts",
9 | "scripts": {
10 | "build": "rimraf build && rimraf dist && tsc -p tsconfig.json && rollup --config",
11 | "build:dev": "cross-env NODE_ENV=development pnpm build",
12 | "test": "vitest run --coverage",
13 | "test:watch": "vitest -w"
14 | },
15 | "exports": {
16 | ".": {
17 | "require": "./dist/index.js",
18 | "import": "./dist/index.mjs"
19 | }
20 | },
21 | "keywords": [
22 | "unis",
23 | "router"
24 | ],
25 | "files": [
26 | "dist"
27 | ],
28 | "author": "anuoua",
29 | "peerDependencies": {
30 | "@unis/core": "workspace:^"
31 | },
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/anuoua/unis/issues"
35 | },
36 | "homepage": "https://github.com/anuoua/unis/tree/main/packages/unis-router",
37 | "devDependencies": {
38 | "@rollup/plugin-node-resolve": "^15.0.2",
39 | "@types/node": "^18.15.11",
40 | "@unis/core": "workspace:^",
41 | "@unis/vite-preset": "workspace:^",
42 | "@vitest/coverage-c8": "^0.29.8",
43 | "cross-env": "^7.0.3",
44 | "esbuild": "^0.17.15",
45 | "rimraf": "^4.4.1",
46 | "rollup": "^3.20.2",
47 | "rollup-plugin-dts": "^5.3.0",
48 | "rollup-plugin-esbuild": "^5.0.0",
49 | "rollup-plugin-reassign": "^1.0.3",
50 | "typescript": "^5.0.3",
51 | "vitest": "^0.29.8"
52 | },
53 | "dependencies": {
54 | "history": "^5.3.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/packages/unis-router/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "rollup-plugin-esbuild";
2 | import dts from "rollup-plugin-dts";
3 | import { defineConfig } from "rollup";
4 | import { nodeResolve } from "@rollup/plugin-node-resolve";
5 | import { reassign } from "rollup-plugin-reassign";
6 | import { unisFns } from "@unis/core";
7 |
8 | const configGen = (format) =>
9 | defineConfig({
10 | input: "src/index.ts",
11 | external: [/^@unis/, "history"],
12 | output: [
13 | {
14 | dir: "dist",
15 | entryFileNames: `index.${format === "esm" ? "mjs" : "js"}`,
16 | format,
17 | sourcemap: true,
18 | },
19 | ],
20 | plugins: [
21 | nodeResolve(),
22 | esbuild({
23 | sourceMap: true,
24 | target: "esnext",
25 | jsx: "automatic",
26 | jsxImportSource: "@unis/core",
27 | }),
28 | reassign({
29 | include: ["**/*.(t|j)s?(x)"],
30 | targetFns: {
31 | "@unis/core": unisFns,
32 | },
33 | }),
34 | ],
35 | });
36 |
37 | const dtsRollup = () =>
38 | defineConfig({
39 | input: "build/index.d.ts",
40 | output: [{ file: "dist/index.d.ts", format: "es" }],
41 | plugins: [dts()],
42 | });
43 |
44 | const config = [configGen("cjs"), configGen("esm"), dtsRollup()];
45 |
46 | export default config;
47 |
--------------------------------------------------------------------------------
/packages/unis-router/src/components/BrowserRouter.tsx:
--------------------------------------------------------------------------------
1 | import { useProps, useLayoutEffect, useState, useMemo } from "@unis/core";
2 | import { createBrowserHistory, BrowserHistory } from "history";
3 | import { LocationContext, RouterContext } from "../context";
4 |
5 | export interface BrowserRouterProps {
6 | children?: JSX.Element | JSX.Element[];
7 | history?: BrowserHistory;
8 | basename?: string;
9 | }
10 |
11 | export const BrowserRouter = (p: BrowserRouterProps) => {
12 | let { children, history, basename } = useProps(p);
13 |
14 | const historyInstance = history ?? createBrowserHistory();
15 |
16 | let [location, setLocation] = useState(historyInstance.location);
17 |
18 | const navigationContextValue = {
19 | basename: basename ?? "",
20 | history: historyInstance,
21 | };
22 |
23 | let locationContextValue = useMemo(
24 | () => ({ location }),
25 | () => [location]
26 | );
27 |
28 | useLayoutEffect(
29 | () => {
30 | const unlisten = historyInstance.listen(({ location }) => {
31 | setLocation(location);
32 | });
33 | return () => unlisten();
34 | },
35 | () => []
36 | );
37 |
38 | return () => (
39 |
40 |
41 | {children}
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/packages/unis-router/src/components/Link.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AnchorHTMLAttributes,
3 | ElementAttrs,
4 | use,
5 | useContext,
6 | useProps,
7 | } from "@unis/core";
8 | import { RouterContext } from "../context";
9 | import { uTargetPath } from "../hooks/uTargetPath";
10 |
11 | export type LinkProps = Partial<
12 | Omit, "href"> & {
13 | to: string;
14 | }
15 | >;
16 |
17 | export const Link = (p: LinkProps) => {
18 | let { to, children, onClick, ...rest } = useProps(p);
19 |
20 | let { history } = useContext(RouterContext);
21 |
22 | let targetPath = use(uTargetPath(() => ({ to })));
23 |
24 | const handleJump = (e: MouseEvent) => {
25 | const target = (e.target as HTMLAnchorElement).target;
26 | if (
27 | e.button === 0 &&
28 | (!target || target === "_self") &&
29 | !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
30 | ) {
31 | if (history.location.pathname !== targetPath) {
32 | history.push(targetPath);
33 | } else {
34 | history.replace(targetPath);
35 | }
36 | e.preventDefault();
37 | }
38 | onClick?.(e);
39 | };
40 |
41 | return () => (
42 |
43 | {children}
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/packages/unis-router/src/components/NavLink.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes, use, useContext, useProps } from "@unis/core";
2 | import { RouteContext } from "../context";
3 | import { uTargetPath } from "../hooks/uTargetPath";
4 | import { Link, LinkProps } from "./Link";
5 |
6 | export type LinkStyle = (isActive: boolean) => HTMLAttributes["style"];
7 | export type LinkClassName = (isActive: boolean) => HTMLAttributes["className"];
8 |
9 | export type NavLinkProps = Omit & {
10 | style?: LinkStyle;
11 | className?: LinkClassName;
12 | };
13 |
14 | export const NavLink = (p: NavLinkProps) => {
15 | let { style, className, to, ...rest } = useProps(p);
16 | let { matches } = useContext(RouteContext);
17 | let targetPath = use(uTargetPath(() => ({ to })));
18 |
19 | let isActive = use(() => !!matches.find((i) => i.pathname === targetPath));
20 |
21 | return () => (
22 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/packages/unis-router/src/components/Outlet.tsx:
--------------------------------------------------------------------------------
1 | import { use, useContext } from "@unis/core";
2 | import { RouteContext } from "../context";
3 |
4 | export const Outlet = () => {
5 | let { matches, route } = useContext(RouteContext);
6 |
7 | let nextRoute = use(() => matches[matches.findIndex((i) => i === route) + 1]);
8 |
9 | let { element, ...rest } = use(() => route ?? {});
10 |
11 | return () =>
12 | route ? (
13 |
14 | {element}
15 |
16 | ) : null;
17 | };
18 |
--------------------------------------------------------------------------------
/packages/unis-router/src/components/Redirect.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useProps } from "@unis/core";
2 | import { RouteContext, RouterContext } from "../context";
3 | import { resolvePath } from "../utils";
4 |
5 | export interface RedirectProps {
6 | to?: string;
7 | replace?: boolean;
8 | }
9 |
10 | export const Redirect = (p: RedirectProps) => {
11 | let { to, replace = true } = useProps(p);
12 | let { history } = useContext(RouterContext);
13 | let { route } = useContext(RouteContext);
14 | useEffect(
15 | () => {
16 | if (route) {
17 | const pathname = resolvePath(route.pathname!, to ?? "");
18 | replace ? history.replace(pathname) : history.push(pathname);
19 | }
20 | },
21 | () => []
22 | );
23 | return () => null;
24 | };
25 |
--------------------------------------------------------------------------------
/packages/unis-router/src/components/Route.tsx:
--------------------------------------------------------------------------------
1 | import { RouteData } from "../types";
2 |
3 | export type RouteProps = Omit & {
4 | children?: JSX.Element | JSX.Element[];
5 | };
6 |
7 | export const Route = (p: RouteProps) => null;
8 |
--------------------------------------------------------------------------------
/packages/unis-router/src/components/Routes.tsx:
--------------------------------------------------------------------------------
1 | import { cloneElement, FiberNode, use, useProps } from "@unis/core";
2 | import { uRouter } from "../hooks/uRouter";
3 | import { Route } from "./Route";
4 | import { RouteData } from "../types";
5 |
6 | export type RoutesProps = Omit & {
7 | children?: JSX.Element | JSX.Element[];
8 | };
9 |
10 | export const Routes = (p: RoutesProps) => {
11 | let { children, path, element: incomeElement } = useProps(p);
12 |
13 | let realChildren = use(() => flatChildren(children));
14 | let routes = use(() => realChildren.map((node) => pick(node)));
15 | let element = use(
16 | uRouter(() => [
17 | {
18 | path,
19 | element: incomeElement,
20 | children: routes,
21 | } as RouteData,
22 | ])
23 | );
24 |
25 | function pick(node: FiberNode): RouteData {
26 | return {
27 | ...node.props,
28 | path: node.props.path,
29 | element: cloneElement(node.props.element),
30 | children: flatChildren(node.props.children).map(pick),
31 | };
32 | }
33 |
34 | function flatChildren(children: JSX.Element | JSX.Element[]) {
35 | return ([] as FiberNode[])
36 | .concat(children as FiberNode[])
37 | .filter((child) => child?.tag === Route);
38 | }
39 |
40 | return () => element;
41 | };
42 |
--------------------------------------------------------------------------------
/packages/unis-router/src/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "@unis/core";
2 | import { BrowserHistory, Location } from "history";
3 | import { MatchRoute } from "./types";
4 |
5 | export interface RouteContextValue {
6 | route: MatchRoute;
7 | matches: MatchRoute[];
8 | }
9 |
10 | export const RouteContext = createContext({
11 | route: undefined!,
12 | matches: [],
13 | });
14 |
15 | export interface RouterContextValue {
16 | history: BrowserHistory;
17 | basename: string;
18 | }
19 |
20 | export const RouterContext = createContext(undefined!);
21 |
22 | export interface LocationContextValue {
23 | location: Location;
24 | }
25 |
26 | export const LocationContext = createContext(undefined!);
27 |
--------------------------------------------------------------------------------
/packages/unis-router/src/hooks/uHistory.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from "@unis/core";
2 | import { RouterContext } from "../context";
3 |
4 | export const uHistory = () => {
5 | let { history } = useContext(RouterContext);
6 | return () => history!;
7 | };
8 |
--------------------------------------------------------------------------------
/packages/unis-router/src/hooks/uLocation.ts:
--------------------------------------------------------------------------------
1 | import { use } from "@unis/core";
2 | import { uHistory } from "./uHistory";
3 |
4 | export const uLocation = () => {
5 | let { location } = use(uHistory());
6 | return () => location;
7 | };
8 |
--------------------------------------------------------------------------------
/packages/unis-router/src/hooks/uParams.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from "@unis/core";
2 | import { RouteContext } from "../context";
3 |
4 | export const uParams = >() => {
5 | let { route } = useContext(RouteContext);
6 | return () => route?.params as unknown as T;
7 | };
8 |
--------------------------------------------------------------------------------
/packages/unis-router/src/hooks/uRouter.tsx:
--------------------------------------------------------------------------------
1 | import { use, useContext } from "@unis/core";
2 | import { RouteContext, RouterContext } from "../context";
3 | import { Outlet } from "../components/Outlet";
4 | import { RouteData } from "../types";
5 | import { matchRoutes } from "../utils";
6 |
7 | export const uRouter = (configFn: () => RouteData[]) => {
8 | let routerData = use(configFn);
9 | let { history, basename } = useContext(RouterContext);
10 |
11 | let location = use(() => history?.location!);
12 |
13 | let wrapedRouterData = use(() => [
14 | {
15 | path: basename,
16 | element: ,
17 | children: routerData,
18 | } as RouteData,
19 | ]);
20 |
21 | let matches = use(() => matchRoutes(location.pathname, wrapedRouterData));
22 |
23 | return () => (
24 |
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/packages/unis-router/src/hooks/uTargetPath.tsx:
--------------------------------------------------------------------------------
1 | import { use, useContext } from "@unis/core";
2 | import { RouteContext } from "../context";
3 | import { resolvePath } from "../utils";
4 |
5 | export interface Options {
6 | to?: string;
7 | }
8 |
9 | export const uTargetPath = (opts: () => Options) => {
10 | let { to } = use(opts);
11 | let { route } = useContext(RouteContext);
12 |
13 | let targetPath = use(() => resolvePath(route.pathname ?? "", to ?? ""));
14 |
15 | return () => targetPath;
16 | };
17 |
--------------------------------------------------------------------------------
/packages/unis-router/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./components/BrowserRouter";
2 | export * from "./context";
3 | export * from "./components/Routes";
4 | export * from "./components/Route";
5 | export * from "./components/Outlet";
6 | export * from "./components/Link";
7 | export * from "./components/NavLink";
8 | export * from "./components/Redirect";
9 | export * from "./hooks/uHistory";
10 | export * from "./hooks/uRouter";
11 | export * from "./hooks/uLocation";
12 | export * from "./hooks/uHistory";
13 | export * from "./hooks/uParams";
14 |
--------------------------------------------------------------------------------
/packages/unis-router/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface RouteData {
2 | path?: string;
3 | element?: JSX.Element;
4 | children?: RouteData[];
5 | }
6 |
7 | export interface MatchRoute extends RouteData {
8 | pathname?: string;
9 | params?: Record;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/unis-router/src/utils.test.tsx:
--------------------------------------------------------------------------------
1 | import { expect, it } from "vitest";
2 | import { RouteData } from "./types";
3 | import { matchRoutes, resolvePath } from "./utils";
4 |
5 | const getRoutes = (): RouteData[] => [
6 | {
7 | path: "/",
8 | children: [
9 | {
10 | path: "home/:from/",
11 | children: [
12 | { path: "*" },
13 | {
14 | path: "post/:id",
15 | },
16 | {
17 | path: "post/1",
18 | children: [
19 | {
20 | children: [{ path: "xx" }],
21 | },
22 | ],
23 | },
24 | ],
25 | },
26 | ],
27 | },
28 | ];
29 |
30 | it("root match", () => {
31 | const matchedRoutes = matchRoutes("/home/www", getRoutes());
32 | expect(matchedRoutes).toMatchObject([
33 | { path: "/", params: {}, pathname: "/" },
34 | {
35 | path: "home/:from/",
36 | params: { from: "www" },
37 | pathname: "/home/www",
38 | },
39 | ]);
40 | });
41 |
42 | it("match test1", () => {
43 | const matchedRoutes = matchRoutes("/home/www/post", getRoutes());
44 | expect(matchedRoutes).toMatchObject([
45 | { path: "/", params: {}, pathname: "/" },
46 | {
47 | path: "home/:from/",
48 | params: { from: "www" },
49 | pathname: "/home/www",
50 | },
51 | { path: "*", params: { from: "www" }, pathname: "/home/www/post" },
52 | ]);
53 | });
54 |
55 | it("match test2", () => {
56 | const matchedRoutes = matchRoutes("/home/www/post/1", getRoutes());
57 | expect(matchedRoutes).toMatchObject([
58 | { path: "/", params: {}, pathname: "/" },
59 | {
60 | path: "home/:from/",
61 | params: { from: "www" },
62 | pathname: "/home/www",
63 | },
64 | {
65 | path: "post/1",
66 | params: { from: "www" },
67 | pathname: "/home/www/post/1",
68 | },
69 | { params: { from: "www" }, pathname: "/home/www/post/1" },
70 | ]);
71 | });
72 |
73 | it("match test3", () => {
74 | const matchedRoutes = matchRoutes("/home/www/post/2", getRoutes());
75 | expect(matchedRoutes).toMatchObject([
76 | { path: "/", params: {}, pathname: "/" },
77 | {
78 | path: "home/:from/",
79 | params: { from: "www" },
80 | pathname: "/home/www",
81 | },
82 | {
83 | path: "post/:id",
84 | params: { from: "www", id: "2" },
85 | pathname: "/home/www/post/2",
86 | },
87 | ]);
88 | });
89 |
90 | it("match test4", () => {
91 | const matchedRoutes = matchRoutes("/home/www/post/1/x", getRoutes());
92 | expect(matchedRoutes).toMatchObject([
93 | { path: "/", params: {}, pathname: "/" },
94 | {
95 | path: "home/:from/",
96 | params: { from: "www" },
97 | pathname: "/home/www",
98 | },
99 | {
100 | path: "*",
101 | params: { from: "www" },
102 | pathname: "/home/www/post/1/x",
103 | },
104 | ]);
105 | });
106 |
107 | it("match test5", () => {
108 | const matchedRoutes = matchRoutes("/home/www/post/1/xx", getRoutes());
109 | expect(matchedRoutes).toMatchObject([
110 | { path: "/", params: {}, pathname: "/" },
111 | {
112 | path: "home/:from/",
113 | params: { from: "www" },
114 | pathname: "/home/www",
115 | },
116 | {
117 | path: "post/1",
118 | params: { from: "www" },
119 | pathname: "/home/www/post/1",
120 | },
121 | {
122 | params: { from: "www" },
123 | pathname: "/home/www/post/1",
124 | },
125 | {
126 | path: "xx",
127 | params: { from: "www" },
128 | pathname: "/home/www/post/1/xx",
129 | },
130 | ]);
131 | });
132 |
133 | it("match test6", () => {
134 | const matchedRoutes = matchRoutes("/app/home/www/post/1/x", getRoutes(), [
135 | { path: "/app" },
136 | ]);
137 | expect(matchedRoutes).toMatchObject([
138 | { path: "/app", params: {}, pathname: "/app" },
139 | { path: "/", params: {}, pathname: "/app" },
140 | {
141 | path: "home/:from/",
142 | params: { from: "www" },
143 | pathname: "/app/home/www",
144 | },
145 | {
146 | path: "*",
147 | params: { from: "www" },
148 | pathname: "/app/home/www/post/1/x",
149 | },
150 | ]);
151 | });
152 |
153 | it("resolvePath", () => {
154 | const path = resolvePath("/home/c", "../a");
155 | expect(path).toBe("/a");
156 | const path2 = resolvePath("/home/c", "./a");
157 | expect(path2).toBe("/home/a");
158 | const path3 = resolvePath("/home/c", "/a");
159 | expect(path3).toBe("/a");
160 | });
161 |
--------------------------------------------------------------------------------
/packages/unis-router/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { MatchRoute, RouteData } from "./types";
2 |
3 | const SLASH = "/";
4 | const DOT = ".";
5 |
6 | const trimSlash = (str: string) => {
7 | const reg = new RegExp(`^[${SLASH}${DOT}]*`);
8 | const reg2 = new RegExp(`[${SLASH}${DOT}]*$`);
9 | return str.replace(reg, "").replace(reg2, "");
10 | };
11 |
12 | const split = (path: string) =>
13 | trimSlash(path) ? trimSlash(path).split(SLASH) : [];
14 |
15 | const analysePath = (locationPathname: string, routePath: string) => {
16 | const locationChunks = split(locationPathname);
17 | const routeChunks = split(routePath);
18 |
19 | const params: Record = {};
20 |
21 | for (let i = 0; i < routeChunks.length; i++) {
22 | const routeChunk = routeChunks[i];
23 | if (routeChunk.startsWith(":")) {
24 | params[routeChunk.slice(1)] = locationChunks[i];
25 | }
26 | }
27 |
28 | return {
29 | params,
30 | pathname:
31 | SLASH +
32 | (locationChunks
33 | .slice(
34 | 0,
35 | routeChunks?.at(-1) === "*" ? undefined : routeChunks?.length ?? 0
36 | )
37 | .join(SLASH) ?? ""),
38 | };
39 | };
40 |
41 | const testPath = (
42 | locationPathname: string,
43 | routePath: string,
44 | final = false
45 | ) => {
46 | const locationChunks = split(locationPathname);
47 | const routeChunks = split(routePath);
48 |
49 | for (let i = 0; i < routeChunks.length; i++) {
50 | const routeChunk = routeChunks[i]!;
51 | const locationChunk = locationChunks[i];
52 |
53 | if (
54 | !locationChunk ||
55 | (!routeChunk.startsWith(":") &&
56 | routeChunk !== "*" &&
57 | routeChunk !== locationChunk)
58 | ) {
59 | return false;
60 | }
61 | }
62 |
63 | if (final && locationChunks.length > routeChunks.length) {
64 | return routeChunks.at(-1) === "*";
65 | }
66 |
67 | return true;
68 | };
69 |
70 | const getScore = (locationPathname: string, routeChain: RouteData[]) => {
71 | const routePath = resolveRoutesPath(routeChain);
72 | const locationChunks = split(locationPathname);
73 | const routeChunks = split(routePath);
74 | let score = 0;
75 |
76 | routeChunks.forEach((chunk, index) => {
77 | let base = 10 ^ ((locationChunks.length ?? 0) - index);
78 | if (chunk === "*") score += 1 * base;
79 | if (chunk.startsWith(":")) score += 2 * base;
80 | if (chunk === locationChunks[index]) score += 3 * base;
81 | });
82 | return score;
83 | };
84 |
85 | const resolveRoutesPath = (routes: RouteData[]) =>
86 | routes.reduce((pre, cur) => {
87 | const path = trimSlash(cur.path ?? "");
88 | return `${pre}${path ? SLASH + path : ""}`;
89 | }, "") || "/";
90 |
91 | const isLocationEnd = (locationPathname: string, routePath: string) => {
92 | return split(locationPathname).length === split(routePath).length;
93 | };
94 |
95 | const matchRoute = (
96 | locationPathname: string,
97 | route: RouteData,
98 | parentRouteChain: RouteData[] = []
99 | ) => {
100 | const routeStack: RouteData[] = parentRouteChain;
101 | let matchedRouteChains: RouteData[][] = [];
102 |
103 | const walk = (route: RouteData) => {
104 | const routeChain = [...routeStack, route];
105 | const routePath = resolveRoutesPath(routeChain);
106 | const isEnd = (route.children?.length ?? 0) === 0;
107 | const result = testPath(locationPathname, routePath, isEnd);
108 |
109 | if (result) {
110 | if (isEnd) {
111 | matchedRouteChains.push(routeChain);
112 | } else {
113 | const preChainSize = matchedRouteChains.length;
114 | route.children?.forEach((childRoute) => {
115 | routeStack.push(route);
116 | walk(childRoute);
117 | routeStack.pop();
118 | });
119 | const afterChainSize = matchedRouteChains.length;
120 | if (
121 | afterChainSize === preChainSize &&
122 | isLocationEnd(locationPathname, routePath)
123 | ) {
124 | matchedRouteChains.push(routeChain);
125 | }
126 | }
127 | }
128 | };
129 |
130 | walk(route);
131 |
132 | return matchedRouteChains;
133 | };
134 |
135 | export const matchRoutes = (
136 | locationPathname: string,
137 | routes: RouteData[],
138 | parentRouteChain: RouteData[] = []
139 | ) => {
140 | const matchedRouteChains: RouteData[][] = [];
141 | routes.forEach((route) => {
142 | matchedRouteChains.push(
143 | ...matchRoute(locationPathname, route, parentRouteChain)
144 | );
145 | });
146 | matchedRouteChains.sort((a, b) => {
147 | const result =
148 | getScore(locationPathname, b) - getScore(locationPathname, a);
149 | if (result !== 0) return result;
150 | return b.length - a.length;
151 | });
152 | const finalChain = matchedRouteChains.at(0) ?? [];
153 | if (finalChain) {
154 | finalChain.forEach((route, index) => {
155 | const subChain = finalChain.slice(0, index + 1);
156 | const { params, pathname } = analysePath(
157 | locationPathname,
158 | resolveRoutesPath(subChain)
159 | );
160 | (route as MatchRoute).params = params;
161 | (route as MatchRoute).pathname = pathname;
162 | });
163 | }
164 | return finalChain;
165 | };
166 |
167 | export const resolvePath = (from: string, to: string) =>
168 | new URL(to, `http://x${from}`).pathname;
169 |
--------------------------------------------------------------------------------
/packages/unis-router/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "build"
5 | },
6 | "include": ["src"]
7 | }
--------------------------------------------------------------------------------
/packages/unis-router/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 | import { unisPreset } from "@unis/vite-preset";
3 |
4 | export default defineConfig({
5 | plugins: [unisPreset()],
6 | test: {
7 | include: ["**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
8 | coverage: {
9 | reporter: ["text", "json", "html"],
10 | },
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/packages/unis-transition/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
--------------------------------------------------------------------------------
/packages/unis-transition/README.md:
--------------------------------------------------------------------------------
1 | # Unis Transition
2 |
3 | Transition component for unis inspired by `React Transition Group`.
4 |
5 | ## Install
6 |
7 | ```shell
8 | npm i @unis/transition
9 | ```
10 |
11 | ## Usage
12 |
13 | ```javascript
14 | import { useState } from '@unis/unis';
15 | import { CSSTransition, TransitionGroup } from '@unis/transition'
16 |
17 | const App = () => {
18 | let [visible, setVisible] = useState(false);
19 |
20 | const handleToggle = () => {
21 | setVisible(!visible);
22 | }
23 |
24 | return () => (
25 |
26 | toggle
27 |
28 |
29 | hello
30 |
31 |
32 |
33 | )
34 | }
35 | ```
36 |
37 | ```css
38 | .fade-appear {
39 | opacity: 0;
40 | }
41 |
42 | .fade-appear-active {
43 | opacity: 1;
44 | transition: all 0.4s ease;
45 | }
46 |
47 | .fade-appear-done {
48 | opacity: 1;
49 | }
50 |
51 | .fade-enter {
52 | opacity: 0;
53 | }
54 |
55 | .fade-enter-active {
56 | opacity: 1;
57 | transition: all 0.4s ease;
58 | }
59 |
60 | .fade-enter-done {
61 | opacity: 1;
62 | }
63 |
64 | .fade-exit {
65 | opacity: 1;
66 | }
67 |
68 | .fade-exit-active {
69 | opacity: 0;
70 | transition: all 0.4s ease;
71 | }
72 |
73 | .fade-exit-done {
74 | opacity: 0;
75 | }
76 | ```
77 |
78 | Online [demo](https://stackblitz.com/edit/vitejs-vite-4cfy2b) here
79 |
80 | ## CSSTransition
81 |
82 | Component API as close to `React Transition Group` as possible.
83 |
84 | ### in
85 |
86 | Show the component.
87 |
88 | type: `boolean`
89 | default: `false`
90 |
91 | ### mountOnEnter
92 |
93 | By default the child component is mounted on the first `in={true}`. you can set `mountOnEnter={false}` child component will be mounted immediately with parent component.
94 |
95 | type: `boolean`
96 | default: `true`
97 |
98 | ### unmountOnExit
99 |
100 | By default the child component is unmounted after it finishes exiting. you can set `unmountOnExit={false}` child component stays mounted after it reaches the 'exited' state.
101 |
102 | type: `boolean`
103 | default: `true`
104 |
105 | ### classNames
106 |
107 | type:
108 |
109 | ```typescript
110 | string | {
111 | appear?: string,
112 | appearActive?: string,
113 | appearDone?: string,
114 | enter?: string,
115 | enterActive?: string,
116 | enterDone?: string,
117 | exit?: string,
118 | exitActive?: string,
119 | exitDone?: string,
120 | }
121 | ```
122 |
123 | default: `''`
124 |
125 | for example `classNames="fade"` it will apply classes below
126 |
127 | - `fade-appear`, `fade-appear-active`, `fade-appear-done`
128 | - `fade-enter`, `fade-enter-active`, `fade-enter-done`
129 | - `fade-exit`, `fade-exit-active`, `fade-exit-done`
130 |
131 | ### onEnter
132 |
133 | Callback fired immediately after the 'enter' or 'appear' class is applied.
134 |
135 | type: `Function(node: HtmlElement)`
136 |
137 | ### onEntering
138 |
139 | Callback fired immediately after the 'enter-active' or 'appear-active' class is applied.
140 |
141 | type: `Function(node: HtmlElement)`
142 |
143 | ### onEntered
144 |
145 | Callback fired immediately after the 'enter' or 'appear' classes are removed and the done class is added to the DOM node.
146 |
147 | type: `Function(node: HtmlElement)`
148 |
149 | ### onExit
150 |
151 | Callback fired immediately after the 'exit' class is applied.
152 |
153 | type: `Function(node: HtmlElement)`
154 |
155 | ### onExiting
156 |
157 | Callback fired immediately after the 'exit-active' is applied.
158 |
159 | type: `Function(node: HtmlElement)`
160 |
161 | ### onExited
162 |
163 | Callback fired immediately after the 'exit' classes are removed and the exit-done class is added to the DOM node.
164 |
165 | type: `Function(node?: HtmlElement)`
166 |
167 | ## TransitionGroup
168 |
169 | Easy to use, just wrap on `CSSTransition` with key.
170 |
171 | ```javascript
172 |
173 | {list.map((item, index) => (
174 |
175 | {item.name}
176 |
177 | ))}
178 |
179 | ```
180 |
181 |
--------------------------------------------------------------------------------
/packages/unis-transition/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@unis/transition",
3 | "version": "0.1.0",
4 | "description": "Unis transition component",
5 | "main": "dist/index.js",
6 | "module": "dist/index.mjs",
7 | "types": "dist/index.d.ts",
8 | "typings": "dist/index.d.ts",
9 | "scripts": {
10 | "build": "rimraf build && rimraf dist && tsc -p tsconfig.json && rollup --config",
11 | "build:dev": "cross-env NODE_ENV=development pnpm build"
12 | },
13 | "exports": {
14 | ".": {
15 | "require": "./dist/index.js",
16 | "import": "./dist/index.mjs"
17 | }
18 | },
19 | "keywords": [
20 | "transition",
21 | "animation",
22 | "unis"
23 | ],
24 | "files": [
25 | "dist"
26 | ],
27 | "author": "anuoua",
28 | "peerDependencies": {
29 | "@unis/core": "workspace:^"
30 | },
31 | "license": "MIT",
32 | "bugs": {
33 | "url": "https://github.com/anuoua/unis/issues"
34 | },
35 | "homepage": "https://github.com/anuoua/unis/tree/main/packages/unis-transition",
36 | "devDependencies": {
37 | "@rollup/plugin-node-resolve": "^15.0.2",
38 | "@unis/core": "workspace:^",
39 | "cross-env": "^7.0.3",
40 | "esbuild": "^0.17.15",
41 | "rimraf": "^4.4.1",
42 | "rollup": "^3.20.2",
43 | "rollup-plugin-dts": "^5.3.0",
44 | "rollup-plugin-esbuild": "^5.0.0",
45 | "rollup-plugin-reassign": "^1.0.3",
46 | "typescript": "^5.0.3"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/unis-transition/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import dts from "rollup-plugin-dts";
2 | import esbuild from "rollup-plugin-esbuild";
3 | import { defineConfig } from "rollup";
4 | import { nodeResolve } from "@rollup/plugin-node-resolve";
5 | import { reassign } from "rollup-plugin-reassign";
6 | import { unisFns } from "@unis/core";
7 |
8 | const configGen = (format) =>
9 | defineConfig({
10 | input: "src/index.ts",
11 | external: [/^@unis/],
12 | output: [
13 | {
14 | dir: "dist",
15 | entryFileNames: `index.${format === "esm" ? "mjs" : "js"}`,
16 | format,
17 | sourcemap: true,
18 | },
19 | ],
20 | plugins: [
21 | nodeResolve(),
22 | esbuild({
23 | sourceMap: true,
24 | target: "esnext",
25 | jsx: "automatic",
26 | jsxImportSource: "@unis/core",
27 | }),
28 | reassign({
29 | include: ["**/*.(t|j)s?(x)"],
30 | targetFns: {
31 | "@unis/core": unisFns,
32 | },
33 | }),
34 | ],
35 | });
36 |
37 | const dtsRollup = () =>
38 | defineConfig({
39 | input: "build/index.d.ts",
40 | output: [{ file: "dist/index.d.ts", format: "es" }],
41 | plugins: [dts()],
42 | });
43 |
44 | const config = [configGen("cjs"), configGen("esm"), dtsRollup()];
45 |
46 | export default config;
47 |
--------------------------------------------------------------------------------
/packages/unis-transition/src/CSSTransition.ts:
--------------------------------------------------------------------------------
1 | import { findEls, use, useLayoutEffect, useProps } from "@unis/core";
2 | import { uInstance } from "./hooks/uInstance";
3 | import {
4 | APPEARED,
5 | APPEARING,
6 | ENTERED,
7 | ENTERING,
8 | EXITED,
9 | EXITING,
10 | TransitionTimeout,
11 | uTransition,
12 | } from "./hooks/uTransition";
13 | import { uWatch } from "./hooks/uWatch";
14 |
15 | export interface TransitionProps {
16 | children: JSX.Element;
17 | classNames: string;
18 | timeout?: TransitionTimeout;
19 | in?: boolean;
20 | mountOnEnter?: boolean;
21 | unmountOnExit?: boolean;
22 | appear?: boolean;
23 | enter?: boolean;
24 | onEnter?: (el: HTMLElement) => void;
25 | onEntering?: (el: HTMLElement) => void;
26 | onEntered?: (el: HTMLElement) => void;
27 | onExit?: (el: HTMLElement) => void;
28 | onExiting?: (el: HTMLElement) => void;
29 | onExited?: (el?: HTMLElement) => void;
30 | }
31 |
32 | export const CSSTransition = (p: TransitionProps) => {
33 | let {
34 | children,
35 | timeout,
36 | in: inProp,
37 | classNames,
38 | unmountOnExit,
39 | mountOnEnter,
40 | appear,
41 | enter,
42 | onEnter,
43 | onEntering,
44 | onEntered,
45 | onExit,
46 | onExiting,
47 | onExited,
48 | } = useProps(p);
49 |
50 | let [instance] = use(uInstance());
51 |
52 | let { childrenState, status } = use(
53 | uTransition(() => ({
54 | in: inProp,
55 | timeout,
56 | enter,
57 | unmountOnExit,
58 | mountOnEnter,
59 | appear,
60 | }))
61 | );
62 |
63 | let currentChildren = use(() => (childrenState ? children : null));
64 |
65 | let cls = use(() => ({
66 | appear: `${classNames}-appear`,
67 | appearActive: `${classNames}-appear-active`,
68 | appearDone: `${classNames}-appear-done`,
69 | enter: `${classNames}-enter`,
70 | enterActive: `${classNames}-enter-active`,
71 | enterDone: `${classNames}-enter-done`,
72 | exit: `${classNames}-exit`,
73 | exitActive: `${classNames}-exit-active`,
74 | exitDone: `${classNames}-exit-done`,
75 | }));
76 |
77 | let el: HTMLElement | undefined;
78 |
79 | const getElement = () => {
80 | const els = findEls(instance, true).filter(
81 | (el) => el instanceof HTMLElement
82 | ) as HTMLElement[];
83 | return els[0] as HTMLElement | undefined;
84 | };
85 |
86 | const clearAll = () => {
87 | if (!el) return;
88 | el.classList.remove(
89 | cls.appear,
90 | cls.appearActive,
91 | cls.appearDone,
92 | cls.enter,
93 | cls.enterActive,
94 | cls.enterDone,
95 | cls.exit,
96 | cls.exitActive,
97 | cls.exitDone
98 | );
99 | };
100 |
101 | const entered = (appear: boolean) => {
102 | if (!el) return;
103 | clearAll();
104 | el.classList.add(appear ? cls.appearDone : cls.enterDone);
105 | onEntered?.(el);
106 | };
107 |
108 | const exited = () => {
109 | if (el) {
110 | clearAll();
111 | el.classList.add(cls.exitDone);
112 | }
113 | onExited?.(el);
114 | };
115 |
116 | const entering = (reflow = false, apear: boolean) => {
117 | if (!el) return;
118 |
119 | clearAll();
120 |
121 | el.classList.add(apear ? cls.appear : cls.enter);
122 |
123 | onEnter?.(el);
124 |
125 | reflow && forceReflow(el);
126 |
127 | clearAll();
128 |
129 | el.classList.add(apear ? cls.appearActive : cls.enterActive);
130 |
131 | onEntering?.(el);
132 | };
133 |
134 | const exiting = (reflow = false) => {
135 | if (!el) return;
136 |
137 | clearAll();
138 |
139 | el.classList.add(cls.exit);
140 |
141 | onExit?.(el);
142 |
143 | reflow && forceReflow(el);
144 |
145 | clearAll();
146 |
147 | el.classList.add(cls.exitActive);
148 |
149 | onExiting?.(el);
150 | };
151 |
152 | useLayoutEffect(
153 | () => {
154 | el = getElement();
155 | },
156 | () => [status, inProp]
157 | );
158 |
159 | uWatch(
160 | (current, previous) => {
161 | switch (current) {
162 | case APPEARING:
163 | entering(previous !== EXITING, true);
164 | break;
165 | case APPEARED:
166 | entered(true);
167 | break;
168 | case ENTERING:
169 | entering(previous !== EXITING, false);
170 | break;
171 | case EXITING:
172 | exiting(previous !== ENTERING);
173 | break;
174 | case ENTERED:
175 | entered(false);
176 | break;
177 | case EXITED:
178 | exited();
179 | break;
180 | }
181 | },
182 | () => [status]
183 | );
184 |
185 | return () => currentChildren;
186 | };
187 |
188 | const forceReflow = (el: HTMLElement) => el.scrollTop;
189 |
--------------------------------------------------------------------------------
/packages/unis-transition/src/TransitionGroup.ts:
--------------------------------------------------------------------------------
1 | import { FiberNode, use, useEffect, useProps, useState } from "@unis/core";
2 | import { CSSTransition } from "./CSSTransition";
3 |
4 | export interface TransitionGroupProps {
5 | children: JSX.Element | JSX.Element[];
6 | }
7 |
8 | export const TransitionGroup = (p: TransitionGroupProps) => {
9 | let { children } = useProps(p);
10 |
11 | let [transitionChildren, setTransitionChildren] = useState([]);
12 |
13 | let flatChildren = use(() =>
14 | ([] as FiberNode[])
15 | .concat(children as unknown as FiberNode)
16 | .filter((child) => child.tag === CSSTransition)
17 | );
18 |
19 | let childrenKeys = use(() =>
20 | flatChildren.map((child) => child.props.key).join(",")
21 | );
22 |
23 | const remove = (key: string) => {
24 | setTransitionChildren(
25 | transitionChildren.filter((child) => child.props.key !== key)
26 | );
27 | };
28 |
29 | const handleChildren = () => {
30 | const flatMap: Record = {};
31 | flatChildren.forEach((child) => {
32 | flatMap[child.props.key] = child;
33 | });
34 |
35 | const transitionMap: Record = {};
36 | transitionChildren.forEach((child) => {
37 | transitionMap[child.props.key] = child;
38 | });
39 |
40 | const newFlatChildren = flatChildren.map((child) => {
41 | const existingNode = transitionMap[child.props.key];
42 | if (existingNode) return existingNode;
43 | const newChild = { ...child };
44 | const originProps = newChild.props;
45 | const onExited = newChild.props.onExited;
46 | newChild.props = {
47 | ...originProps,
48 | in: true,
49 | onExited: (...args: unknown[]) => {
50 | onExited?.(...args);
51 | remove(originProps.key);
52 | },
53 | };
54 | return newChild;
55 | });
56 |
57 | transitionChildren.forEach((child, index) => {
58 | const newChild = { ...child };
59 | if (!flatMap[newChild.props.key]) {
60 | newChild.props = {
61 | ...newChild.props,
62 | in: false,
63 | };
64 | newFlatChildren.splice(index, 0, newChild);
65 | }
66 | });
67 |
68 | setTransitionChildren(newFlatChildren);
69 | };
70 |
71 | useEffect(
72 | () => {
73 | handleChildren();
74 | },
75 | () => [childrenKeys]
76 | );
77 |
78 | return () => transitionChildren;
79 | };
80 |
--------------------------------------------------------------------------------
/packages/unis-transition/src/hooks/uInstance.ts:
--------------------------------------------------------------------------------
1 | import { Fiber, use } from "@unis/core";
2 |
3 | export const uInstance = () => {
4 | let instance = use((WF: Fiber) => WF);
5 | return () => [instance];
6 | };
7 |
--------------------------------------------------------------------------------
/packages/unis-transition/src/hooks/uTransition.ts:
--------------------------------------------------------------------------------
1 | import { use, useEffect, useState } from "@unis/core";
2 |
3 | export const UNMOUNTED = "unmounted";
4 | export const APPEARING = "appearing";
5 | export const APPEARED = "appeared";
6 | export const ENTERING = "entering";
7 | export const ENTERED = "entered";
8 | export const EXITING = "exiting";
9 | export const EXITED = "exited";
10 |
11 | export type TimeoutObject = {
12 | appear?: number;
13 | enter?: number;
14 | exit?: number;
15 | };
16 |
17 | export type TransitionTimeout = number | TimeoutObject;
18 |
19 | export interface uTransitionProps {
20 | in?: boolean;
21 | enter?: boolean;
22 | unmountOnExit?: boolean;
23 | mountOnEnter?: boolean;
24 | appear?: boolean;
25 | timeout?: TransitionTimeout;
26 | }
27 |
28 | export const uTransition = (optsFn: () => uTransitionProps) => {
29 | let {
30 | in: inProp = false,
31 | enter = true,
32 | unmountOnExit = true,
33 | mountOnEnter = true,
34 | timeout = 0,
35 | appear = false,
36 | } = use(optsFn);
37 |
38 | let timer: number;
39 | let mounted = false;
40 |
41 | let realMountOnEnter = use(() => (unmountOnExit ? true : mountOnEnter));
42 | let timeoutObject = use(() => {
43 | if (typeof timeout === "number") {
44 | return {
45 | appear: timeout,
46 | enter: timeout,
47 | exit: timeout,
48 | } as TimeoutObject;
49 | } else {
50 | const enter = timeout.enter ?? 0;
51 | return {
52 | appear: enter,
53 | enter,
54 | exit: timeout.exit ?? 0,
55 | } as TimeoutObject;
56 | }
57 | });
58 |
59 | let [childrenState, setChildrenState] = useState(
60 | !realMountOnEnter ? true : inProp
61 | );
62 |
63 | let [initialStatus] = useState(
64 | childrenState && appear && inProp
65 | ? enter
66 | ? APPEARING
67 | : ENTERED
68 | : UNMOUNTED
69 | );
70 |
71 | useEffect(
72 | () => {
73 | if (childrenState && initialStatus === APPEARING) {
74 | setTimer(timeoutObject.appear!);
75 | }
76 | },
77 | () => []
78 | );
79 |
80 | let [status, setStatus] = useState(initialStatus);
81 |
82 | const switchChildren = () => {
83 | if (status === ENTERING) setStatus(ENTERED);
84 | if (status === EXITING) setStatus(EXITED);
85 | if (status === APPEARING) setStatus(APPEARED);
86 | setChildrenState(inProp ? true : !unmountOnExit);
87 | };
88 |
89 | function setTimer(statusTimeout: number) {
90 | clearTimeout(timer);
91 | timer = setTimeout(() => {
92 | switchChildren();
93 | }, statusTimeout);
94 | }
95 |
96 | useEffect(
97 | () => {
98 | if (!mounted) {
99 | mounted = true;
100 | return;
101 | }
102 | if (inProp) {
103 | if (!enter) return setStatus(ENTERED);
104 | setStatus(ENTERING);
105 | setChildrenState(true);
106 | setTimer(timeoutObject.enter!);
107 | } else {
108 | setStatus(EXITING);
109 | setTimer(timeoutObject.exit!);
110 | }
111 | },
112 | () => [inProp]
113 | );
114 |
115 | return () => ({
116 | childrenState,
117 | status,
118 | });
119 | };
120 |
--------------------------------------------------------------------------------
/packages/unis-transition/src/hooks/uUpdate.ts:
--------------------------------------------------------------------------------
1 | import { useReducer } from "@unis/core";
2 |
3 | export const uUpdate = () => {
4 | let [, dispatch] = useReducer((a) => a + 1, 0);
5 |
6 | const update = () => dispatch(undefined);
7 |
8 | return () => [update];
9 | };
10 |
--------------------------------------------------------------------------------
/packages/unis-transition/src/hooks/uWatch.ts:
--------------------------------------------------------------------------------
1 | import { use, useLayoutEffect } from "@unis/core";
2 |
3 | export const uWatch = (
4 | handler: (currentValue: T, previousValue: T | undefined) => void,
5 | depsFn: () => [T]
6 | ) => {
7 | let [value] = use(depsFn);
8 |
9 | let preValue: T | undefined = undefined;
10 |
11 | useLayoutEffect(
12 | () => {
13 | handler(value, preValue);
14 | preValue = value;
15 | },
16 | () => [value]
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/packages/unis-transition/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./CSSTransition";
2 | export * from "./TransitionGroup";
3 | export * from "./hooks/uTransition";
4 |
--------------------------------------------------------------------------------
/packages/unis-transition/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "build"
5 | },
6 | "include": ["src"]
7 | }
--------------------------------------------------------------------------------
/packages/unis-vite-preset/.gitignore:
--------------------------------------------------------------------------------
1 | build/
--------------------------------------------------------------------------------
/packages/unis-vite-preset/README.md:
--------------------------------------------------------------------------------
1 | # Unis Vite Preset
2 |
3 | Unis develop preset for vite.
4 |
5 | ## Install
6 |
7 | ```shell
8 | npm add -D @unis/vite-preset
9 | ```
10 |
11 | ## Usage
12 |
13 | vite.config.js
14 |
15 | ```javascript
16 | import { defineConfig } from "vite";
17 | import { unisPreset } from '@unis/vite-preset'
18 |
19 | export default defineConfig({
20 | plugins: [
21 | unisPreset()
22 | ]
23 | });
24 | ```
25 |
26 |
--------------------------------------------------------------------------------
/packages/unis-vite-preset/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@unis/vite-preset",
3 | "version": "0.1.0",
4 | "description": "Unis vite preset",
5 | "main": "build/index.js",
6 | "module": "build/index.mjs",
7 | "types": "build/index.d.ts",
8 | "typings": "build/index.d.ts",
9 | "scripts": {
10 | "build": "rimraf build && rollup --config && tsc",
11 | "build:dev": "cross-env NODE_ENV=development pnpm build"
12 | },
13 | "exports": {
14 | ".": {
15 | "require": "./build/index.js",
16 | "import": "./build/index.mjs"
17 | }
18 | },
19 | "keywords": [
20 | "vite",
21 | "preset",
22 | "unis"
23 | ],
24 | "files": [
25 | "build"
26 | ],
27 | "author": "anuoua",
28 | "peerDependencies": {
29 | "@unis/core": "workspace:^",
30 | "vite": "^4.2.1"
31 | },
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/anuoua/unis/issues"
35 | },
36 | "homepage": "https://github.com/anuoua/unis/tree/main/packages/unis-vite-preset",
37 | "dependencies": {
38 | "@callback-reassign/rollup-plugin": "^0.0.1"
39 | },
40 | "devDependencies": {
41 | "@rollup/plugin-node-resolve": "^13.0.6",
42 | "@unis/core": "workspace:^",
43 | "cross-env": "^7.0.3",
44 | "esbuild": "^0.13.13",
45 | "rimraf": "^3.0.2",
46 | "rollup": "^2.72.0",
47 | "rollup-plugin-esbuild": "^4.6.0",
48 | "typescript": "^4.4.4",
49 | "vite": "^4.2.1"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/unis-vite-preset/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "rollup";
2 | import { nodeResolve } from "@rollup/plugin-node-resolve";
3 | import esbuild from "rollup-plugin-esbuild";
4 |
5 | const configGen = (format) =>
6 | defineConfig({
7 | input: "src/index.ts",
8 | external: [/^@unis/, "@callback-reassign/rollup-plugin"],
9 | output: [
10 | {
11 | dir: "build",
12 | entryFileNames: `index.${format === "esm" ? "mjs" : "js"}`,
13 | format,
14 | sourcemap: true,
15 | },
16 | ],
17 | plugins: [
18 | nodeResolve(),
19 | esbuild({
20 | sourceMap: true,
21 | minify: process.env.NODE_ENV === "development" ? false : true,
22 | target: "esnext",
23 | }),
24 | ],
25 | });
26 |
27 | const config = [configGen("cjs"), configGen("esm")];
28 |
29 | export default config;
30 |
--------------------------------------------------------------------------------
/packages/unis-vite-preset/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { PluginOption } from "vite";
2 | import { reassign } from "@callback-reassign/rollup-plugin";
3 | import { unisFns } from "@unis/core";
4 |
5 | export function unisPreset(): PluginOption[] {
6 | return [
7 | {
8 | name: "unis-preset",
9 |
10 | enforce: "pre",
11 |
12 | config(config) {
13 | return {
14 | esbuild: {
15 | jsx: "automatic",
16 | jsxImportSource: "@unis/core",
17 | ...config.esbuild,
18 | },
19 | };
20 | },
21 | },
22 | reassign({
23 | include: ["**/*.(t|j)s?(x)"],
24 | targetFns: {
25 | "@unis/core": unisFns,
26 | },
27 | }) as PluginOption,
28 | ];
29 | }
30 |
--------------------------------------------------------------------------------
/packages/unis-vite-preset/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "emitDeclarationOnly": true,
5 | "declarationMap": false,
6 | "outDir": "build"
7 | },
8 | "include": ["src"]
9 | }
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/**"
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": [
5 | "DOM",
6 | "ESNext"
7 | ],
8 | "jsx": "react-jsx",
9 | "jsxImportSource": "@unis/core",
10 | "module": "ESNext",
11 | "moduleResolution": "node",
12 | "declaration": true,
13 | "emitDeclarationOnly": true,
14 | "sourceMap": true,
15 | "esModuleInterop": true,
16 | "forceConsistentCasingInFileNames": true,
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "skipLibCheck": false
20 | },
21 | }
22 |
--------------------------------------------------------------------------------