├── .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 | [![@unis/core CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-core.yml/badge.svg)](https://github.com/anuoua/unis/actions/workflows/unis-core.yml) [![@unis/dom CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-dom.yml/badge.svg)](https://github.com/anuoua/unis/actions/workflows/unis-dom.yml) [![@unis/router CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-router.yml/badge.svg)](https://github.com/anuoua/unis/actions/workflows/unis-router.yml) [![@unis/transition CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-transition.yml/badge.svg)](https://github.com/anuoua/unis/actions/workflows/unis-transition.yml) [![@unis/vite-preset CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-vite-preset.yml/badge.svg)](https://github.com/anuoua/unis/actions/workflows/unis-vite-preset.yml) [![@unis/babel-preset CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-babel-preset.yml/badge.svg)](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 | [![@unis/core CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-core.yml/badge.svg)](https://github.com/anuoua/unis/actions/workflows/unis-core.yml) [![@unis/dom CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-dom.yml/badge.svg)](https://github.com/anuoua/unis/actions/workflows/unis-dom.yml) [![@unis/router CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-router.yml/badge.svg)](https://github.com/anuoua/unis/actions/workflows/unis-router.yml) [![@unis/transition CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-transition.yml/badge.svg)](https://github.com/anuoua/unis/actions/workflows/unis-transition.yml) [![@unis/vite-preset CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-vite-preset.yml/badge.svg)](https://github.com/anuoua/unis/actions/workflows/unis-vite-preset.yml) [![@unis/babel-preset CI/CD](https://github.com/anuoua/unis/actions/workflows/unis-babel-preset.yml/badge.svg)](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
Bpp: light
Cpp: gray
Dpp: light
Epp: initial
" 88 | ); 89 | 90 | await rendered(); 91 | 92 | expect(root.innerHTML).toBe( 93 | "
App
Bpp: dark
Cpp: gray
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("
hello
0
"); 49 | await rendered(); 50 | expect(root.innerHTML).toBe("
hello world
0
"); 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
hello dialog
" 46 | ); 47 | await rendered(); 48 | expect(document.body.innerHTML).toBe( 49 | "
hello
" 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 | '
123
del
4
5
6
' 57 | ); 58 | await rendered(); 59 | expect(root.innerHTML).toBe( 60 | '
1
5
423
6
' 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}` 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 | 50 |
51 |
52 | 59 | 65 | 66 |
67 | {" "} 68 | Delete {item?.name} ? 69 |
70 |
71 |
72 | 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 |
    App
    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 | 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 | --------------------------------------------------------------------------------