Repository: anuoua/unis
Branch: main
Commit: a40e220ce9e6
Files: 145
Total size: 203.9 KB
Directory structure:
gitextract_322wan4z/
├── .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/
│ └── 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
│ │ │ ├── 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-workspace.yaml
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/unis-babel-preset.yml
================================================
name: "@unis/babel-preset CI/CD"
on:
push:
branches:
- 'main'
paths:
- 'packages/unis-babel-preset/**'
jobs:
"test":
runs-on: ubuntu-latest
if: ${{contains(github.event.head_commit.message, '(babel-preset):')}}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v2.0.1
with:
version: 7.9.0
- run: |
pnpm install
cd packages/unis-core
pnpm build
cd ../unis-babel-preset
pnpm test
"publish":
needs: test
runs-on: ubuntu-latest
if: ${{startsWith(github.event.head_commit.message, 'release(babel-preset):')}}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v2.0.1
with:
version: 7.9.0
- run: |
pnpm install
cd packages/unis-core
pnpm build
cd ../unis-babel-preset
pnpm build
pnpm publish --no-git-checks --access public
env:
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .github/workflows/unis-core.yml
================================================
name: "@unis/core CI/CD"
on:
push:
branches:
- 'main'
paths:
- 'packages/unis-core/**'
jobs:
"test":
runs-on: ubuntu-latest
if: ${{contains(github.event.head_commit.message, '(core):')}}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v2.0.1
with:
version: 7.9.0
- run: |
pnpm install
cd packages/unis-core
pnpm test
"publish":
needs: test
runs-on: ubuntu-latest
if: ${{startsWith(github.event.head_commit.message, 'release(core):')}}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v2.0.1
with:
version: 7.1.2
- run: |
pnpm install
cd packages/unis-core
pnpm build
pnpm publish --no-git-checks --access public
env:
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .github/workflows/unis-dom.yml
================================================
name: "@unis/dom CI/CD"
on:
push:
branches:
- 'main'
paths:
- 'packages/unis-dom/**'
jobs:
"test":
runs-on: ubuntu-latest
if: ${{contains(github.event.head_commit.message, '(dom):')}}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v2.0.1
with:
version: 7.9.0
- run: |
pnpm install
cd packages/unis-core
pnpm build
cd ../unis-vite-preset
pnpm build
cd ../unis-dom
pnpm test
"publish":
needs: test
runs-on: ubuntu-latest
if: ${{startsWith(github.event.head_commit.message, 'release(dom):')}}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v2.0.1
with:
version: 7.1.2
- run: |
pnpm install
cd packages/unis-core
pnpm build
cd ../unis-dom
pnpm build
pnpm publish --no-git-checks --access public
env:
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .github/workflows/unis-router.yml
================================================
name: "@unis/router CI/CD"
on:
push:
branches:
- 'main'
paths:
- 'packages/unis-router/**'
jobs:
"test":
runs-on: ubuntu-latest
if: ${{contains(github.event.head_commit.message, '(router):')}}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v2.0.1
with:
version: 7.9.0
- run: |
pnpm install
cd packages/unis-core
pnpm build
cd ../unis-vite-preset
pnpm build
cd ../unis-router
pnpm test
"publish":
needs: test
runs-on: ubuntu-latest
if: ${{startsWith(github.event.head_commit.message, 'release(router):')}}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v2.0.1
with:
version: 7.9.0
- run: |
pnpm install
cd packages/unis-core
pnpm build
cd ../unis-router
pnpm build
pnpm publish --no-git-checks --access public
env:
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .github/workflows/unis-transition.yml
================================================
name: "@unis/transition CI/CD"
on:
push:
branches:
- 'main'
paths:
- 'packages/unis-transition/**'
jobs:
"publish":
runs-on: ubuntu-latest
if: ${{startsWith(github.event.head_commit.message, 'release(transition):')}}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v2.0.1
with:
version: 7.9.0
- run: |
pnpm install
cd packages/unis-core
pnpm build
cd ../unis-transition
pnpm build
pnpm publish --no-git-checks --access public
env:
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .github/workflows/unis-vite-preset.yml
================================================
name: "@unis/vite-preset CI/CD"
on:
push:
branches:
- 'main'
paths:
- 'packages/unis-vite-preset/**'
jobs:
"publish":
runs-on: ubuntu-latest
if: ${{startsWith(github.event.head_commit.message, 'release(vite-preset):')}}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v2.0.1
with:
version: 7.9.0
- run: |
pnpm install
cd packages/unis-core
pnpm build
cd ../unis-vite-preset
pnpm build
pnpm publish --no-git-checks --access public
env:
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
# Dependency directories
node_modules/
.DS_Store
================================================
FILE: .vscode/settings.json
================================================
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescriptreact]": {
"editor.formatOnSave": true
},
"[typescript]": {
"editor.formatOnSave": true
},
"[javascript]": {
"editor.formatOnSave": true
},
"[markdown]": {
"editor.formatOnSave": true
},
"editor.tabSize": 2,
"editor.insertSpaces": true,
"files.associations": {
"*.json": "jsonc"
}
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021-present anuoua
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README-zh_CN.md
================================================
[](https://github.com/anuoua/unis/actions/workflows/unis-core.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-dom.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-router.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-transition.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-vite-preset.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-babel-preset.yml)
# Unis
Unis 是一款新的前端框架,创新的编译策略打造的组件 API 帮助你更加轻松的创建网页 UI。
## 性能
## 安装
```bash
npm i @unis/core @unis/dom
```
## Vite 开发
```shell
npm i vite @unis/vite-preset -D
```
vite.config.js
```javascript
import { defineConfig } from "vite";
import { unisPreset } from "@unis/vite-preset";
export default defineConfig({
plugins: [unisPreset()],
});
```
tsconfig.json
```json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@unis/core"
}
}
```
index.html
```javascript
...
```
index.tsx
```javascript
function App() {
return () => hello
;
}
render( , document.querySelector("#root"));
```
## 用法
Unis 并不是 React 的复刻,而是保留了 React 使用体验的全新框架,unis 的用法很简单,熟悉 React 的可以很快上手。
### 组件
在 unis 中组件是一个高阶函数。
```javascript
import { render } from "@unis/dom";
const App = () => {
return () => (
// 返回一个函数
hello world
);
};
render( , document.querySelector("#root"));
```
### 组件状态
Unis 中的 `useState` 用法和 React 相似,但是要注意的是 unis 中 `use` 系列方法,定义类型必须为 `let` ,因为 unis 使用了 Callback Reassign 编译策略,[@callback-reassign/rollup-plugin](https://github.com/anuoua/callback-reassign) 帮我们补全了 Callback Reassign 代码。
```javascript
import { useState } from "@unis/core";
const App = () => {
let [msg, setMsg] = useState("hello");
/**
* Compile to:
*
* let [msg, setMsg] = useState('hello', ([$0, $1]) => { msg = $0; setMsg = $1 });
*/
return () => {msg}
;
};
```
### Props
Unis 直接使用 props 会无法获取最新值,所以 unis 提供了 useProps。
```javascript
import { useProps } from "@unis/core";
const App = (p) => {
let { some } = useProps(p);
/**
* Compile to:
*
* let { some } = useProps(p, ({ some: $0 }) => { some = $0 });
*/
return () => {some}
;
};
```
### 副作用
Unis 保留了和 React 基本一致的 `useEffect` 和 `useLayoutEffect` ,但 deps 是一个返回数组的函数。
```javascript
import { useEffect } from "@unis/core";
const App = () => {
useEffect(
() => {
// ...
return () => {
// 清理...
};
},
() => [] // deps 是一个返回数组的函数
);
return () => hello
;
};
```
### 自定义 hook
Unis 的自定义 hook ,在有返回值的场景需要搭配 `use` 方法使用,原因则是前面提到的 Callback Reassign 编译策略。自定义 hook 的命名我们约定以小写字母 `u` 开头,目的是用于区分其他函数,同时在 IDE 的提示下更加方便的导入。
```javascript
import { use, useState } from "@unis/core";
// 创建自定义 hook 高阶函数
const uCount = () => {
let [count, setCount] = useState(0);
const add = () => setCount(count + 1);
return () => [count, add];
};
// 通过 `use` 使用 hook
function App() {
let [count, add] = use(uCount());
/**
* Compile to:
*
* let [count, add] = use(uCount(), ([$0, $1]) => { count = $0; add = $1 });
*/
return () => {count}
;
}
```
## 特性
### Fragment
```javascript
import { Fragment } from "@unis/core";
function App() {
return () => (
);
}
```
### Portal
```javascript
import { createPortal } from "@unis/core";
function App() {
return () => createPortal(
, document.body);
}
```
### Context
```javascript
import { createContext } from "@unis/core";
import { render } from "@unis/dom";
const ThemeContext = createContext("light");
function App() {
let theme = useContext(ThemeContext);
return () => {theme}
;
}
render(
,
document.querySelector("#root")
);
```
## SSR 服务端渲染
服务端
```javascript
import express from "express";
import { renderToString } from "@unis/dom/server";
const app = express();
app.get("/", (req, res) => {
const SSR_CONTENT = renderToString(hello world
);
res.send(`
${SSR_CONTENT}
`);
});
```
客户端
```javascript
import { render } from "@unis/dom";
render(
,
document.querySelector("#root"),
true // true 代表使用 hydrate (水合)进行渲染,复用 server 端的内容。
);
```
## Todo 项目
完整项目请查看
- [packages/unis-example](packages/unis-example) Todo 示例
- [stackbliz](https://stackblitz.com/edit/vitejs-vite-8hn3pz) 试用
## API
- Core
- h
- h2 (for jsx2)
- Fragment
- createPortal
- createContext
- render
- memo
- Hooks
- use
- useProps
- useState
- useReducer
- useContext
- useMemo
- useEffect
- useRef
- useId
## License
MIT @anuoua
================================================
FILE: README.md
================================================
[](https://github.com/anuoua/unis/actions/workflows/unis-core.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-dom.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-router.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-transition.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-vite-preset.yml) [](https://github.com/anuoua/unis/actions/workflows/unis-babel-preset.yml)
# Unis [中文](./README-zh_CN.md)
Unis is a new front-end framework. Its innovative compilation strategy and component API built help you create web UI more easily.
## Performance
## Installation
```bash
npm i @unis/core @unis/dom
```
## Vite Development
```shell
npm i vite @unis/vite-preset -D
```
vite.config.js
```javascript
import { defineConfig } from "vite";
import { unisPreset } from "@unis/vite-preset";
export default defineConfig({
plugins: [unisPreset()],
});
```
tsconfig.json
```json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@unis/core"
}
}
```
index.html
```javascript
...
```
index.tsx
```javascript
function App() {
return () => hello
;
}
render( , document.querySelector("#root"));
```
## Usage
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.
### Components
In Unis, the component is a higher-order function.
```javascript
import { render } from "@unis/dom";
const App = () => {
return () => (
// Returns a function
hello world
);
};
render( , document.querySelector("#root"));
```
### Component State
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.
```javascript
import { useState } from "@unis/core";
const App = () => {
let [msg, setMsg] = useState("hello");
/**
* Compile to:
*
* let [msg, setMsg] = useState('hello', ([$0, $1]) => { msg = $0; setMsg = $1 });
*/
return () => {msg}
;
};
```
### Props
Directly using `props` in Unis will be unable to get the latest value, so Unis provides `useProps`.
```javascript
import { useProps } from "@unis/core";
const App = (p) => {
let { some } = useProps(p);
/**
* Compile to:
*
* let { some } = useProps(p, ({ some: $0 }) => { some = $0 });
*/
return () => {some}
;
};
```
### Side Effects
Unis retains the familiar `useEffect` and `useLayoutEffect` methods from React, but the `deps` parameter is a function that returns an array.
```javascript
import { useEffect } from "@unis/core";
const App = () => {
useEffect(
() => {
// ...
return () => {
// Clean up...
};
},
() => [] // deps is a function that returns an array
);
return () => hello
;
};
```
### Custom Hook
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.
```javascript
import { use, useState } from "@unis/core";
// Create a higher-order function for the custom hook
const uCount = () => {
let [count, setCount] = useState(0);
const add = () => setCount(count + 1);
return () => [count, add];
};
// Use the hook through `use`
function App() {
let [count, add] = use(uCount());
/**
* Compile to:
*
* let [count, add] = use(uCount(), ([$0, $1]) => { count = $0; add = $1 });
*/
return () => {count}
;
}
```
## Features
### Fragment
```javascript
import { Fragment } from "@unis/core";
function App() {
return () => (
);
}
```
### Portal
```javascript
import { createPortal } from "@unis/core";
function App() {
return () => createPortal(
, document.body);
}
```
### Context
```javascript
import { createContext } from "@unis/core";
import { render } from "@unis/dom";
const ThemeContext = createContext("light");
function App() {
let theme = useContext(ThemeContext);
return () => {theme}
;
}
render(
,
document.querySelector("#root")
);
```
## Server-Side Rendering
Server
```javascript
import express from "express";
import { renderToString } from "@unis/dom/server";
const app = express();
app.get("/", (req, res) => {
const SSR_CONTENT = renderToString(hello world
);
res.send(`
${SSR_CONTENT}
`);
});
```
Client
```javascript
import { render } from "@unis/dom";
render(
,
document.querySelector("#root"),
true // true means using hydration to render and reuse the server-side rendered content.
);
```
## Todo Project
See complete project at
- [packages/unis-example](packages/unis-example) Todo example
- [stackbliz](https://stackblitz.com/edit/vitejs-vite-8hn3pz) Try it out
## API
- Core
- h
- h2 (for jsx2)
- Fragment
- createPortal
- createContext
- render
- memo
- Hooks
- use
- useProps
- useState
- useReducer
- useContext
- useMemo
- useEffect
- useRef
- useId
## License
MIT @anuoua
================================================
FILE: assets/logo.txt
================================================
████
██ █
█ █
███ █ █
█ ████ ██████
█ ██ ██ ██
██ ███
█ █ █ ██
██ ███
█ █
██ ████
██████████
================================================
FILE: package.json
================================================
{
"name": "unis",
"version": "0.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"workspaces": {
"packages": [
"./packages/*"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/anuoua/unis.git"
},
"author": "anuoua",
"license": "MIT",
"bugs": {
"url": "https://github.com/anuoua/unis/issues"
},
"homepage": "https://github.com/anuoua/unis#readme"
}
================================================
FILE: packages/unis-babel-preset/.gitignore
================================================
build/
================================================
FILE: packages/unis-babel-preset/README.md
================================================
# Unis Babel Preset
Unis develop preset for babel.
## Install
```shell
npm add -D @unis/babel-preset
```
## Usage
.babelrc.json or babel.config.js
```javascript
{
"presets": ["@unis/babel-preset"]
}
```
If you use @babel/preset-env, please use relatively new targets. There is a bug in the babel transformation of destructuring syntax. e.g.
```javascript
{
"presets": [
[
"@babel/preset-env",
{
targets: "> 0.25%, not dead",
}
],
"@unis/babel-preset"
]
}
```
================================================
FILE: packages/unis-babel-preset/package.json
================================================
{
"name": "@unis/babel-preset",
"version": "0.0.2",
"description": "Unis babel preset",
"main": "build/index.js",
"module": "build/index.mjs",
"types": "build/index.d.ts",
"typings": "build/index.d.ts",
"scripts": {
"build": "rimraf build && rollup --config && tsc",
"build:dev": "cross-env NODE_ENV=development pnpm build",
"test": "vitest run"
},
"exports": {
".": {
"require": "./build/index.js",
"import": "./build/index.mjs"
}
},
"keywords": [
"babel",
"preset",
"unis"
],
"files": [
"build"
],
"author": "anuoua",
"license": "MIT",
"bugs": {
"url": "https://github.com/anuoua/unis/issues"
},
"homepage": "https://github.com/anuoua/unis/tree/main/packages/unis-babel-preset",
"peerDependencies": {
"@unis/core": "workspace:^"
},
"dependencies": {
"@babel/plugin-syntax-jsx": "^7.21.4",
"@babel/plugin-transform-react-jsx": "^7.21.0",
"@callback-reassign/babel-plugin": "^0.0.1"
},
"devDependencies": {
"@babel/core": "^7.21.4",
"@rollup/plugin-node-resolve": "^13.0.6",
"@types/babel__core": "^7.20.0",
"@unis/core": "workspace:^",
"cross-env": "^7.0.3",
"esbuild": "^0.13.13",
"rimraf": "^3.0.2",
"rollup": "^2.72.0",
"rollup-plugin-esbuild": "^4.6.0",
"typescript": "^4.4.4",
"vite": "^4.2.1",
"vitest": "^0.29.8"
}
}
================================================
FILE: packages/unis-babel-preset/rollup.config.js
================================================
import { defineConfig } from "rollup";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import esbuild from "rollup-plugin-esbuild";
const configGen = (format) =>
defineConfig({
input: "src/index.ts",
external: [
/^@unis/,
"@callback-reassign/rollup-plugin",
"@babel/plugin-syntax-jsx",
"@babel/plugin-transform-react-jsx",
],
output: [
{
dir: "build",
entryFileNames: `index.${format === "esm" ? "mjs" : "js"}`,
format,
sourcemap: true,
},
],
plugins: [
nodeResolve({
modulesOnly: true,
}),
esbuild({
sourceMap: true,
minify: process.env.NODE_ENV === "development" ? false : true,
target: "esnext",
}),
],
});
const config = [configGen("cjs"), configGen("esm")];
export default config;
================================================
FILE: packages/unis-babel-preset/src/index.ts
================================================
import { unisFns } from "@unis/core";
import reassign from "@callback-reassign/babel-plugin";
// @ts-ignore
import syntaxJsx from "@babel/plugin-syntax-jsx";
// @ts-ignore
import transformReactJsx from "@babel/plugin-transform-react-jsx";
export default function unisPreset() {
return {
plugins: [
syntaxJsx,
[
transformReactJsx,
{
runtime: "automatic",
importSource: "@unis/core",
},
],
[
reassign,
{
targetFns: {
"@unis/core": unisFns,
},
},
],
],
};
}
================================================
FILE: packages/unis-babel-preset/test/index.test.ts
================================================
import { it, expect } from "vitest";
import { transform } from "@babel/core";
import unisPreset from "../src/index";
const code = `
import { useState } from "@unis/core";
let [a, seta] = useState(1);
`;
const transformed = `import { useState } from "@unis/core";
let [a, seta] = useState(1, ([$0, $1]) => {
a = $0;
seta = $1;
});`;
it("transform", () => {
const result = transform(code, {
presets: [unisPreset],
});
expect(result?.code).toBe(transformed);
});
================================================
FILE: packages/unis-babel-preset/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": true,
"declarationMap": false,
"outDir": "build"
},
"include": ["src"]
}
================================================
FILE: packages/unis-core/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
build
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
================================================
FILE: packages/unis-core/index.d.ts
================================================
import "./jsx-runtime/jsx-runtime";
import "./jsx-runtime/jsx-dev-runtime";
export * from "./dist";
================================================
FILE: packages/unis-core/jsx-runtime/jsx-dev-runtime.d.ts
================================================
declare module "@unis/core/jsx-dev-runtime";
================================================
FILE: packages/unis-core/jsx-runtime/jsx-dev-runtime.js
================================================
const { h2, Fragment } = require("../dist/index.js");
exports.jsxDEV = h2;
exports.Fragment = Fragment;
================================================
FILE: packages/unis-core/jsx-runtime/jsx-dev-runtime.mjs
================================================
export { h2 as jsxDEV, Fragment } from "../dist/index.mjs";
================================================
FILE: packages/unis-core/jsx-runtime/jsx-runtime.d.ts
================================================
declare module "@unis/core/jsx-runtime";
================================================
FILE: packages/unis-core/jsx-runtime/jsx-runtime.js
================================================
const { h2, Fragment } = require("../dist/index.mjs");
exports.jsx = h2;
exports.jsxs = h2;
exports.Fragment = Fragment;
================================================
FILE: packages/unis-core/jsx-runtime/jsx-runtime.mjs
================================================
export { h2 as jsx, h2 as jsxs, Fragment } from "../dist/index.mjs";
================================================
FILE: packages/unis-core/package.json
================================================
{
"name": "@unis/core",
"version": "1.2.5",
"description": "Unis is a simpler and easier to use front-end framework than React",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "index.d.ts",
"typings": "index.d.ts",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./jsx-runtime": {
"require": "./jsx-runtime/jsx-runtime.js",
"import": "./jsx-runtime/jsx-runtime.mjs"
},
"./jsx-dev-runtime": {
"require": "./jsx-runtime/jsx-dev-runtime.js",
"import": "./jsx-runtime/jsx-dev-runtime.mjs"
}
},
"scripts": {
"build": "rimraf build && rimraf dist && tsc -p tsconfig.build.json && rollup --config",
"build:dev": "cross-env NODE_ENV=development pnpm build",
"test": "vitest run --coverage",
"test:watch": "vitest -w"
},
"repository": {
"type": "git",
"url": "git+https://github.com/anuoua/unis.git"
},
"keywords": [
"frontend",
"web",
"framwork"
],
"files": [
"dist",
"jsx-runtime",
"index.d.ts"
],
"author": "anuoua",
"license": "MIT",
"bugs": {
"url": "https://github.com/anuoua/unis/issues"
},
"homepage": "https://github.com/anuoua/unis#readme",
"dependencies": {
"@types/prop-types": "^15.7.5",
"@types/scheduler": "^0.16.3",
"csstype": "^3.1.2"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.2",
"@types/jsdom": "^21.1.1",
"@vitest/coverage-c8": "^0.29.8",
"cross-env": "^7.0.3",
"esbuild": "^0.17.15",
"jsdom": "^21.1.1",
"rimraf": "^4.4.1",
"rollup": "^3.20.2",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0",
"tslib": "^2.5.0",
"typescript": "^5.0.3",
"vite": "^4.2.1",
"vitest": "^0.29.8"
}
}
================================================
FILE: packages/unis-core/rollup.config.mjs
================================================
import dts from "rollup-plugin-dts";
import esbuild from "rollup-plugin-esbuild";
import { defineConfig } from "rollup";
import { nodeResolve } from "@rollup/plugin-node-resolve";
const configGen = (format) =>
defineConfig({
input: "src/index.ts",
output: [
{
dir: "dist",
entryFileNames: `index.${format === "esm" ? "mjs" : "js"}`,
format,
sourcemap: true,
},
],
plugins: [
nodeResolve(),
esbuild({
sourceMap: true,
target: "esnext",
}),
],
});
const dtsRollup = () =>
defineConfig({
input: "build/index.d.ts",
output: [{ file: `dist/index.d.ts`, format: "es" }],
plugins: [dts()],
});
const config = [configGen("cjs"), configGen("esm"), dtsRollup()];
export default config;
================================================
FILE: packages/unis-core/src/api/use.ts
================================================
import { getWF } from "./utils";
export function use any>(fn: T): ReturnType;
export function use any>(
fn: T,
raFn: Function
): ReturnType;
export function use any>(fn: T, raFn?: Function) {
const workingFiber = getWF();
const effect = () => {
const result = fn(getWF());
return raFn?.(result);
};
workingFiber.stateEffects?.push(effect) ??
(workingFiber.stateEffects = [effect]);
return fn(workingFiber) as ReturnType;
}
================================================
FILE: packages/unis-core/src/api/useContext.ts
================================================
import { Context } from "../context";
import { Fiber } from "../fiber";
import { use } from "./use";
export function useContext(ctx: T) {
return use(contextHof(ctx), arguments[1]);
}
const contextHof = (context: T) => {
const readContext = (fiber: Fiber): T["initial"] => {
const { dependencyList = [] } = fiber.reconcileState!;
const result = [...dependencyList]
.reverse()
.find((d) => d.context === context);
if (result) {
if (fiber.dependencies) {
!fiber.dependencies.includes(result) && fiber.dependencies.push(result);
} else {
fiber.dependencies = [result];
}
return result.value;
} else {
return context.initial;
}
};
return (WF: Fiber) => readContext(WF);
};
================================================
FILE: packages/unis-core/src/api/useEffect.ts
================================================
import { Effect, EFFECT_TYPE, getWF } from "./utils";
export const useEffect = (cb: Effect, depsFn?: () => any[]) => {
const workingFiber = getWF();
cb.depsFn = depsFn;
if (!cb.type) cb.type = EFFECT_TYPE.TICK;
workingFiber.effects?.push(cb) ?? (workingFiber.effects = [cb]);
};
================================================
FILE: packages/unis-core/src/api/useId.ts
================================================
import { Fiber } from "../fiber";
import { getWF } from "./utils";
import { use } from "./use";
import { generateId } from "../utils";
export const idHof = () => {
let workingFiber = getWF();
if (!workingFiber.id) {
workingFiber.id = generateId();
}
return (WF: Fiber) => WF.id;
};
export function useId() {
return use(idHof());
}
================================================
FILE: packages/unis-core/src/api/useLayoutEffect.ts
================================================
import { useEffect } from "./useEffect";
import { Effect, EFFECT_TYPE } from "./utils";
export const useLayoutEffect = (cb: Effect, depsFn?: () => any[]) => {
cb.type = EFFECT_TYPE.LAYOUT;
return useEffect(cb, depsFn);
};
================================================
FILE: packages/unis-core/src/api/useMemo.ts
================================================
import { use } from "./use";
import { Fiber, MemorizeState } from "../fiber";
import { arraysEqual } from "../utils";
import { addDispatchEffect, linkMemorizeState } from "./useReducer";
export const memoHof = (
handler: () => T,
depsFn?: () => any[]
) => {
let freshFiber: Fiber | undefined;
let freshDeps: any[];
let freshMemorizeState: MemorizeState | undefined;
let memorizeState: MemorizeState = {
value: undefined,
deps: undefined!,
};
const effect = () => {
memorizeState = freshMemorizeState!;
memorizeState.deps = freshDeps;
freshFiber = undefined;
freshMemorizeState = undefined;
};
return (WF: Fiber) => {
freshFiber = WF;
freshDeps = depsFn?.() ?? freshDeps;
addDispatchEffect(freshFiber, effect);
freshMemorizeState = {
value:
depsFn && arraysEqual(memorizeState.deps, freshDeps)
? memorizeState.value
: handler(),
deps: memorizeState?.deps,
};
linkMemorizeState(freshFiber, freshMemorizeState);
return freshMemorizeState.value as T;
};
};
export function useMemo(
handler: () => T,
depsFn?: () => any[]
) {
return use(memoHof(handler, depsFn), arguments[2]);
}
================================================
FILE: packages/unis-core/src/api/useProps.ts
================================================
import { Fiber } from "../fiber";
import { use } from "./use";
export function useProps(p: T) {
return use(propsHof(p), arguments[1]);
}
export const propsHof = (props: T) => {
return (WF: Fiber) => WF.props as T;
};
================================================
FILE: packages/unis-core/src/api/useReducer.ts
================================================
import { Effect, markFiber } from "./utils";
import { use } from "./use";
import { Fiber, findRoot, findRuntime, MemorizeState, TokTik } from "../fiber";
import { readyForWork } from "../reconcile";
export type Reducer = (state: T, action: T2) => T;
const readyList: (() => Fiber)[] = [];
const triggerReconcile = () => {
const fibers = new Set(readyList.map((getFiber) => getFiber()));
fibers.forEach(markFiber);
// multiple app trigger same time
const rootFibers = new Set(Array.from(fibers).map(findRoot));
rootFibers.forEach((fiber) => readyForWork(fiber));
readyList.length = 0;
};
export const reducerHof = (
reducerFn: Reducer,
initial: T
) => {
let currentFiber: Fiber | undefined; // do not getWF here, workingFiber should be assigned in effect.
let freshFiber: Fiber | undefined;
let freshMemorizeState: MemorizeState | undefined;
let toktik: TokTik | undefined;
let memorizeState: MemorizeState = {
value: undefined,
dispatchValue: initial,
deps: [initial],
};
const dispatch = (action: T2) => {
if (!currentFiber) return console.warn("Component is not created");
if (currentFiber.isDestroyed)
return console.warn("Component has been destroyed");
const newState = reducerFn(memorizeState.value, action);
if (Object.is(newState, memorizeState.value)) return;
memorizeState.dispatchValue = newState;
memorizeState.deps = [newState];
if (freshFiber) {
toktik!.clearTikTaskQueue();
}
readyList.push(() => currentFiber!);
if (readyList.length === 1) {
toktik!.addTok(triggerReconcile, true);
}
};
const effect: Effect = () => {
currentFiber = freshFiber!;
memorizeState = freshMemorizeState!;
if (!toktik) toktik = findRuntime(currentFiber).toktik;
freshFiber = undefined;
freshMemorizeState = undefined;
};
return (WF: Fiber) => {
freshFiber = WF;
addDispatchEffect(freshFiber, effect);
freshMemorizeState = {
value:
memorizeState.deps.length > 0
? memorizeState.dispatchValue
: memorizeState.value,
deps: [],
};
linkMemorizeState(freshFiber, freshMemorizeState);
return [freshMemorizeState.value, dispatch] as const;
};
};
export function useReducer(reducerFn: Reducer, initial: T) {
return use(reducerHof(reducerFn, initial), arguments[2]);
}
export const addDispatchEffect = (freshFiber: Fiber, effect: Effect) => {
freshFiber.reconcileState!.dispatchEffectList?.push(effect) ??
(freshFiber.reconcileState!.dispatchEffectList = [effect]);
};
export const linkMemorizeState = (
freshFiber: Fiber,
freshMemorizeState: MemorizeState
) => {
if (freshFiber.memorizeState) {
const first = freshFiber.memorizeState.next;
freshFiber.memorizeState.next = freshMemorizeState;
freshMemorizeState.next = first;
} else {
freshFiber.memorizeState = freshMemorizeState;
freshMemorizeState.next = freshMemorizeState;
}
};
export const cutMemorizeState = (fiber: Fiber) => {
const first = fiber.memorizeState?.next;
fiber.memorizeState && (fiber.memorizeState.next = undefined);
fiber.memorizeState = first;
};
================================================
FILE: packages/unis-core/src/api/useRef.ts
================================================
export interface Ref {
current: T;
}
export function useRef(): Ref;
export function useRef(value: T): Ref;
export function useRef(value?: T) {
return { current: value };
}
================================================
FILE: packages/unis-core/src/api/useState.ts
================================================
import { use } from "./use";
import { reducerHof } from "./useReducer";
export const stateHof = (initial: T) => {
return reducerHof((preState, action) => action, initial);
};
export function useState(): [
T | undefined,
(value: T | undefined) => void
];
export function useState(initial: T): [T, (value: T) => void];
export function useState(initial?: T) {
return use(stateHof(initial), arguments[1]);
}
================================================
FILE: packages/unis-core/src/api/utils.ts
================================================
import { Fiber, FLAG, mergeFlag } from "../fiber";
import { getWorkingFiber } from "../reconcile";
import { arraysEqual } from "../utils";
export enum EFFECT_TYPE {
LAYOUT = "layout",
TICK = "tick",
}
export type Effect = (() => (() => void) | void) & {
type?: EFFECT_TYPE;
clear?: (() => void) | void;
depsFn?: () => any;
deps?: any;
};
export const getWF = (): Fiber | never => {
const workingFiber = getWorkingFiber();
if (workingFiber) {
return workingFiber;
} else {
throw Error("Do not call use function outside of component");
}
};
export const markFiber = (workingFiber: Fiber) => {
workingFiber.flag = mergeFlag(workingFiber.flag, FLAG.UPDATE);
let iFiber: Fiber | undefined = workingFiber;
while ((iFiber = iFiber.parent)) {
if (iFiber.childFlag) break;
iFiber.childFlag = mergeFlag(iFiber.childFlag, FLAG.UPDATE);
}
};
export const runStateEffects = (fiber: Fiber) => {
for (const effect of fiber.stateEffects ?? []) {
effect();
}
};
export const effectDepsEqual = (effect: Effect) => {
const deps = effect.depsFn?.();
const equal = arraysEqual(deps, effect.deps);
effect.deps = deps;
return equal;
};
export const clearEffects = (effects?: Effect[]) => {
if (!effects) return;
for (const effect of effects) {
effect.clear?.();
}
};
export const runEffects = (effects?: Effect[]) => {
if (!effects) return;
for (const effect of effects) {
effect.clear = effect();
}
};
export const clearAndRunEffects = (effects?: Effect[]) => {
if (!effects) return;
for (const effect of effects) {
if (effectDepsEqual(effect)) continue;
effect.clear?.();
effect.clear = effect();
}
};
================================================
FILE: packages/unis-core/src/commit.ts
================================================
import { clearEffects } from "./api/utils";
import {
Fiber,
findEls,
FLAG,
getContainerElFiber,
graft,
isComponent,
isHostElement,
matchFlag,
isText,
isElement,
ReconcileState,
isPortal,
Operator,
} from "./fiber";
export const commitDeletion = (fiber: Fiber, operator: Operator) => {
let iFiber: Fiber | undefined = fiber;
while (iFiber) {
if (isHostElement(iFiber)) {
iFiber.props.ref && (iFiber.props.ref.current = undefined);
}
if (isComponent(iFiber)) {
clearEffects(iFiber.effects);
}
/**
* remove input element may trigger blur sync event,
* so isDestroyed must be true before remove to prevent dispatch in useReducer.
*/
iFiber.isDestroyed = true;
if (isPortal(iFiber)) {
iFiber.child && operator.remove(iFiber.child);
}
iFiber.dependencies = undefined;
iFiber.reconcileState = undefined;
if (iFiber.child) {
iFiber = iFiber.child;
continue;
} else if (iFiber === fiber) {
iFiber = undefined;
continue;
}
while (iFiber) {
if (iFiber.sibling) {
iFiber = iFiber.sibling;
break;
}
if (iFiber.parent !== fiber) {
iFiber = iFiber.parent;
} else {
iFiber = undefined;
break;
}
}
}
operator.remove(fiber);
};
export const commitUpdate = (fiber: Fiber, operator: Operator) => {
if (isText(fiber)) operator.updateTextProperties(fiber);
if (isHostElement(fiber)) operator.updateElementProperties(fiber);
};
export const commitInsert = (fiber: Fiber, operator: Operator) => {
const container = getContainerElFiber(fiber)!;
const insertElements = isElement(fiber)
? [fiber.el!]
: findEls(
matchFlag(fiber.commitFlag, FLAG.REUSE) ? fiber.alternate! : fiber
);
const insertTarget = isPortal(container)
? null
: fiber.preElFiber
? operator.nextSibling(fiber.preElFiber)
: operator.firstChild(container);
for (const insertElement of insertElements) {
operator.insertBefore(container, insertElement, insertTarget);
}
};
export const commit = (reconcileState: ReconcileState) => {
const { operator } = reconcileState.rootWorkingFiber.runtime!;
for (const fiber of reconcileState.commitList) {
if (matchFlag(fiber.commitFlag, FLAG.DELETE)) {
commitDeletion(fiber.alternate!, operator);
continue;
}
if (matchFlag(fiber.commitFlag, FLAG.UPDATE)) {
commitUpdate(fiber, operator);
}
if (matchFlag(fiber.commitFlag, FLAG.INSERT)) {
commitInsert(fiber, operator);
}
if (matchFlag(fiber.commitFlag, FLAG.REUSE)) {
graft(fiber, fiber.alternate!);
}
fiber.preElFiber = undefined;
fiber.alternate = undefined;
fiber.commitFlag = undefined;
}
};
================================================
FILE: packages/unis-core/src/context.ts
================================================
import { useProps } from "./api/useProps";
import { useContext } from "./api/useContext";
import { PROVIDER, Fiber } from "./fiber";
export interface Context {
Provider: (props: { value: T; children: any }) => JSX.Element;
Consumer: (props: { children: (value: T) => JSX.Element }) => JSX.Element;
initial: T;
}
export interface Dependency {
context: Context;
value: T;
}
const providerContextMap = new WeakMap();
export const createDependency = (fiber: Fiber) => {
return {
context: providerContextMap.get(fiber.tag as Function)!,
value: fiber.props.value,
};
};
export const findDependency = (fiber: Fiber, contextFiber: Fiber) =>
fiber.dependencies?.find(
(dependency) =>
dependency.context ===
providerContextMap.get(contextFiber.tag as Function)
);
export function createContext(initial: T): Context;
export function createContext(initial: T) {
const Provider = (props: { value: T; children: any }) => props.children;
Provider.take = {
type: PROVIDER,
};
const Consumer = (props: { children: (value: T) => JSX.Element }) => {
let p = useProps(
props,
// @ts-ignore
($) => (p = $)
);
let state = useContext(
context,
// @ts-ignore
($) => (state = $)
);
return () => p.children(state);
};
const context: Context = {
Provider,
Consumer,
initial,
};
providerContextMap.set(Provider, context);
return context;
}
================================================
FILE: packages/unis-core/src/createTokTik.ts
================================================
export type Task = Function & { isTok?: any };
export const createTokTik = (options: {
nextTick: (cb: VoidFunction, pending: boolean) => void;
now: () => number;
interval?: number;
}) => {
const { nextTick, now, interval = 4 } = options;
const timeSlicing = !!interval;
let lastTime: number = 0;
let looping = false;
const tikQueue: Task[] = [];
const tokQueue: Task[] = [];
const next = () => tikQueue[0] ?? tokQueue[0];
const pick = () => tikQueue.shift() ?? tokQueue.shift();
const loop = (task: Task): void => {
looping = true;
runTask(task);
const nextTask = next();
if (nextTask) {
if (shouldYield() || nextTask.isTok) {
nextTick(() => loop(pick()!), nextTask.isTok);
} else {
loop(pick()!);
}
return;
}
looping = false;
};
const runTask = timeSlicing
? (task: Task) => {
lastTime = now();
return task();
}
: (task: Task) => task();
const addTok = (task: Task, pending = false) => {
task.isTok = true;
looping
? tokQueue.push(task)
: pending
? nextTick(() => loop(task), pending)
: loop(task);
};
const addTik = (task: Task) => {
looping && tikQueue.push(task);
};
const clearTikTaskQueue = () => (tikQueue.length = 0);
const shouldYield = timeSlicing
? () => now() - lastTime > interval
: () => false;
return {
addTok,
addTik,
clearTikTaskQueue,
shouldYield,
};
};
================================================
FILE: packages/unis-core/src/diff.ts
================================================
import {
clearFlag,
Fiber,
FLAG,
isElement,
matchFlag,
isPortal,
isSame,
mergeFlag,
isComponent,
ReconcileState,
isText,
findToRoot,
} from "./fiber";
import {
classes,
isNullish,
isStr,
isEvent,
keys,
styleStr,
svgKey,
} from "./utils";
export type AttrDiff = [string, any, any][];
export const attrDiff = (
newFiber: Record,
oldFiber: Record,
onlyEvent = false
) => {
const diff: AttrDiff = [];
const newProps = newFiber.props;
const oldProps = oldFiber.props;
const getRealAttr = (attr: string) => {
if (attr === "className") return "class";
if (attr === "htmlFor") return "for";
if (newFiber.isSvg) return svgKey(attr);
return attr.toLowerCase();
};
const getRealValue = (newValue: any, key: string) => {
if (isNullish(newValue)) return;
switch (key) {
case "className":
return isStr(newValue) ? newValue : classes(newValue);
case "style":
return isStr(newValue)
? newValue
: styleStr(newValue as Partial);
default:
return newValue;
}
};
for (const key of keys({ ...newProps, ...oldProps })) {
if (onlyEvent && !isEvent(key)) continue;
if (["xmlns", "children"].includes(key)) continue;
const newValue = newProps[key];
const oldValue = oldProps[key];
const realNewValue = getRealValue(newValue, key);
const realOldValue = getRealValue(oldValue, key);
if (
!isNullish(newValue) &&
!isNullish(oldValue) &&
realNewValue === realOldValue
)
continue;
diff.push([getRealAttr(key), realNewValue, realOldValue]);
}
return diff;
};
export const clone = (newFiber: Fiber, oldFiber: Fiber, commitFlag?: FLAG) =>
Object.assign(
{
...newFiber,
commitFlag,
alternate: oldFiber,
},
isComponent(newFiber)
? {
renderFn: oldFiber.renderFn,
rendered: oldFiber.rendered,
stateEffects: oldFiber.stateEffects,
effects: oldFiber.effects,
id: oldFiber.id,
}
: isElement(newFiber)
? { el: oldFiber.el, isSvg: oldFiber.isSvg }
: isPortal(newFiber)
? { to: oldFiber.to }
: undefined
);
export const reuse = (newFiber: Fiber, oldFiber: Fiber, commitFlag?: FLAG) => ({
...newFiber,
commitFlag,
alternate: oldFiber,
});
export const del = (oldFiber: Fiber): Fiber => ({
commitFlag: FLAG.DELETE,
alternate: oldFiber,
});
export const create = (
newFiber: Fiber,
parentFiber: Fiber,
hydrate = false
) => {
const retFiber = {
...newFiber,
commitFlag: matchFlag(parentFiber.commitFlag, FLAG.CREATE)
? FLAG.CREATE
: FLAG.CREATE | FLAG.INSERT,
} as Fiber;
if (isElement(newFiber)) {
retFiber.isSvg = newFiber.tag === "svg" || parentFiber.isSvg;
if (!hydrate) {
retFiber.attrDiff = isText(retFiber)
? undefined
: attrDiff(retFiber, { props: {} });
}
}
if (isPortal(newFiber)) {
retFiber.commitFlag = undefined;
}
return retFiber;
};
export const keyIndexMapGen = (
children: Fiber[],
start: number,
end: number
) => {
const map: any = {};
for (let i = start; i <= end; i++) {
const key = children[i].props?.key;
if (key !== undefined) map[key] = i;
}
return map;
};
const determineCommitFlag = (
parentFiber: Fiber,
newFiber: Fiber,
oldFiber: Fiber,
flag?: FLAG
) => {
/**
* the nearest parent component fiber
*/
const nearestComponent = isComponent(parentFiber)
? parentFiber
: findToRoot(parentFiber, (fiber) => isComponent(fiber));
/**
* when nearest parent component fiber with FLAG.UPDATE commitFlag, it should be FLAG.UPDATE.
*/
let commitFlag =
!matchFlag(nearestComponent?.commitFlag, FLAG.UPDATE) &&
parentFiber.alternate!.childFlag
? !oldFiber.childFlag && !oldFiber.flag
? FLAG.REUSE
: oldFiber.flag
: FLAG.UPDATE;
/**
* when memo fiber compare result is true, it should be FLAG.REUSE.
*/
if (
isComponent(oldFiber) &&
!oldFiber.childFlag &&
!oldFiber.flag &&
(oldFiber.tag as Function & { compare?: Function }).compare?.(
newFiber.props,
oldFiber.props
)
) {
commitFlag = mergeFlag(commitFlag, FLAG.REUSE);
}
if (isElement(newFiber) && matchFlag(commitFlag, FLAG.UPDATE)) {
let diff = attrDiff(newFiber, oldFiber);
if (diff.length) {
newFiber.attrDiff = diff;
} else {
commitFlag = clearFlag(commitFlag, FLAG.UPDATE);
}
}
flag && (commitFlag = mergeFlag(commitFlag, flag));
/**
* portal don't need commitFlag
*/
if (isPortal(newFiber)) {
commitFlag = undefined;
}
if (matchFlag(commitFlag, FLAG.REUSE)) {
commitFlag = clearFlag(commitFlag, FLAG.UPDATE);
}
return commitFlag;
};
const getSameNewFiber = (
parentFiber: Fiber,
newFiber: Fiber,
oldFiber: Fiber,
flag?: FLAG
) => {
const commitFlag = determineCommitFlag(parentFiber, newFiber, oldFiber, flag);
return matchFlag(commitFlag, FLAG.REUSE)
? reuse(newFiber, oldFiber, commitFlag)
: clone(newFiber, oldFiber, commitFlag);
};
export const diff = (
parentFiber: Fiber,
oldChildren: Fiber[] = [],
newChildren: Fiber[] = []
) => {
const { reconcileState } = parentFiber as { reconcileState: ReconcileState };
let cloneChildren: Fiber[] = [];
let newStartIndex = 0;
let newEndIndex = newChildren.length - 1;
let oldStartIndex = 0;
let oldEndIndex = oldChildren.length - 1;
let newStartFiber = newChildren[newStartIndex];
let newEndFiber = newChildren[newEndIndex];
let oldStartFiber = oldChildren[oldStartIndex];
let oldEndFiber = oldChildren[oldEndIndex];
let preStartFiber: Fiber | undefined;
let preEndFiber: Fiber | undefined;
const deletion = (fiber: Fiber) => {
reconcileState.commitList.push(del(fiber));
};
const forward = () => {
if (preStartFiber) preStartFiber.sibling = newStartFiber;
newStartFiber.parent = parentFiber;
newStartFiber.index = newStartIndex;
newStartFiber.reconcileState = reconcileState;
preStartFiber = newStartFiber;
cloneChildren[newStartIndex] = newStartFiber;
newStartFiber = newChildren[++newStartIndex];
};
const forwardEnd = () => {
if (preEndFiber) newEndFiber.sibling = preEndFiber;
newEndFiber.parent = parentFiber;
newEndFiber.index = newEndIndex;
newEndFiber.reconcileState = reconcileState;
preEndFiber = newEndFiber;
cloneChildren[newEndIndex] = newEndFiber;
newEndFiber = newChildren[--newEndIndex];
};
const oldForward = () => {
oldStartFiber = oldChildren[++oldStartIndex];
};
const oldForwardEnd = () => {
oldEndFiber = oldChildren[--oldEndIndex];
};
let keyIndexMap: any;
while (newStartIndex <= newEndIndex && oldStartIndex <= oldEndIndex) {
if (oldStartFiber === undefined) {
oldForward();
} else if (oldEndFiber === undefined) {
oldForwardEnd();
} else if (isSame(newStartFiber, oldStartFiber)) {
newStartFiber = getSameNewFiber(
parentFiber,
newStartFiber,
oldStartFiber
);
forward();
oldForward();
} else if (isSame(newEndFiber, oldEndFiber)) {
newEndFiber = getSameNewFiber(parentFiber, newEndFiber, oldEndFiber);
forwardEnd();
oldForwardEnd();
} else if (isSame(newStartFiber, oldEndFiber)) {
newStartFiber = getSameNewFiber(
parentFiber,
newStartFiber,
oldEndFiber,
FLAG.INSERT
);
forward();
oldForwardEnd();
} else if (isSame(newEndFiber, oldStartFiber)) {
newEndFiber = getSameNewFiber(
parentFiber,
newEndFiber,
oldStartFiber,
FLAG.INSERT
);
forwardEnd();
oldForward();
} else {
if (!keyIndexMap) {
keyIndexMap = keyIndexMapGen(oldChildren, oldStartIndex, oldEndIndex);
}
const index = keyIndexMap[newStartFiber.props.key];
if (isNaN(index)) {
newStartFiber = create(newStartFiber, parentFiber);
} else {
const targetFiber = oldChildren[index];
if (isSame(newStartFiber, targetFiber)) {
newStartFiber = getSameNewFiber(
parentFiber,
newStartFiber,
targetFiber,
FLAG.INSERT
);
oldChildren[index] = undefined as unknown as Fiber;
} else {
newStartFiber = create(newStartFiber, parentFiber);
}
}
forward();
}
}
if (oldStartIndex > oldEndIndex) {
newChildren.slice(newStartIndex, newEndIndex + 1).forEach((fiber) => {
newStartFiber = create(
newStartFiber,
parentFiber,
reconcileState.hydrate
);
forward();
});
} else if (newStartIndex > newEndIndex) {
oldChildren
.slice(oldStartIndex, oldEndIndex + 1)
.forEach((fiber) => fiber && deletion(fiber));
}
if (preStartFiber && preEndFiber) preStartFiber.sibling = preEndFiber;
parentFiber.child = cloneChildren[0];
parentFiber.children = cloneChildren;
};
================================================
FILE: packages/unis-core/src/fiber.ts
================================================
import { Effect } from "./api/utils";
import { Dependency } from "./context";
import { AttrDiff } from "./diff";
import { isFun, isNullish } from "./utils";
export interface ReconcileState {
rootWorkingFiber: Fiber;
dispatchEffectList: Effect[];
commitList: Fiber[];
dependencyList: Dependency[];
workingPreElFiber?: Fiber;
hydrate: boolean;
hydrateEl?: FiberEl;
}
export enum FLAG {
CREATE = 1 << 1,
INSERT = 1 << 2,
UPDATE = 1 << 3,
DELETE = 1 << 4,
REUSE = 1 << 5,
}
export type FlagName = "flag" | "childFlag" | "commitFlag";
export const mergeFlag = (a: FLAG | undefined, b: FLAG) =>
isNullish(a) ? b : a | b;
export const clearFlag = (a: FLAG | undefined, b: FLAG) =>
isNullish(a) ? a : a & ~b;
export const matchFlag = (a: FLAG | undefined, b: FLAG) =>
isNullish(a) ? false : a & b;
export type FiberEl = unknown;
export type FiberType =
| string
| Function
| Symbol
| ((...p: any[]) => () => any);
export interface MemorizeState {
value: any;
dispatchValue?: any;
deps: any[];
next?: MemorizeState;
}
export interface TokTik {
addTok: (task: Function, pending?: boolean) => void;
addTik: (task: Function) => void;
clearTikTaskQueue: () => void;
shouldYield: () => boolean;
}
export interface Operator {
// for reuse element when hydrate
nextElement(el: FiberEl): FiberEl | null;
// for reuse element when hydrate
matchElement(fiber: Fiber, el: FiberEl): boolean;
createElement(fiber: Fiber): FiberEl;
remove(fiber: Fiber): void;
insertBefore(
containerFiber: Fiber,
insertElement: FiberEl,
targetElement: FiberEl | null
): void;
firstChild(fiber: Fiber): FiberEl | null;
nextSibling(fiber: Fiber): FiberEl | null;
updateTextProperties(fiber: Fiber): void;
updateElementProperties(fiber: Fiber): void;
}
export interface Runtime {
toktik: TokTik;
operator: Operator;
}
export interface Fiber {
id?: string;
parent?: Fiber;
child?: Fiber;
sibling?: Fiber;
index?: number;
to?: FiberEl;
el?: FiberEl;
preElFiber?: Fiber;
isSvg?: boolean;
isDestroyed?: boolean;
props?: any;
compare?: Function;
attrDiff?: AttrDiff;
alternate?: Fiber;
tag?: string | Function;
type?: Symbol;
renderFn?: Function;
rendered?: any;
flag?: FLAG;
childFlag?: FLAG;
commitFlag?: FLAG;
children?: Fiber[];
stateEffects?: Effect[];
effects?: Effect[];
dependencies?: Dependency[];
reconcileState?: ReconcileState;
memorizeState?: MemorizeState;
runtime?: Runtime;
}
export const createFiber = (options: Partial = {}) =>
Object.assign(
{
id: undefined,
parent: undefined,
child: undefined,
sibling: undefined,
index: undefined,
to: undefined,
el: undefined,
preElFiber: undefined,
isSvg: undefined,
isDestroyed: undefined,
props: undefined,
compare: undefined,
attrDiff: undefined,
alternate: undefined,
tag: undefined,
type: undefined,
renderFn: undefined,
rendered: undefined,
flag: undefined,
childFlag: undefined,
commitFlag: undefined,
children: undefined,
stateEffects: undefined,
effects: undefined,
dependencies: undefined,
reconcileState: undefined,
memorizeState: undefined,
},
options
);
export const TEXT = Symbol("$$Text");
export const ELEMENT = Symbol("$$Element");
export const PORTAL = Symbol("$$Portal");
export const PROVIDER = Symbol("$$Provider");
export const COMPONENT = Symbol("$$Component");
export const isText = (fiber: Fiber) => fiber.type === TEXT;
export const isHostElement = (fiber: Fiber) => fiber.type === ELEMENT;
export const isElement = (fiber: Fiber) =>
isHostElement(fiber) || isText(fiber);
export const isPortal = (fiber: Fiber) => fiber.type === PORTAL;
export const isProvider = (fiber: Fiber) => fiber.type === PROVIDER;
export const isCustomComponent = (fiber: Fiber) => fiber.type === COMPONENT;
export const isComponent = (fiber: Fiber) => isFun(fiber.tag);
export const isSame = (fiber1?: Fiber, fiber2?: Fiber) =>
fiber1 &&
fiber2 &&
fiber1.tag === fiber2.tag &&
fiber1.props?.key === fiber2.props?.key;
export interface WalkHook {
enter?: (currentFiber: Fiber, skipChild: boolean) => any;
down?: (currentFiber: Fiber, nextFiber: Fiber) => any;
sibling?: (currentFiber: Fiber, nextFiber?: Fiber) => any;
up?: (currentFiber: Fiber, nextFiber?: Fiber) => any;
return?: (currentFiber?: Fiber) => any;
}
export type WalkHookKeys = keyof WalkHook;
export type WalkHookList = {
[K in keyof WalkHook]: WalkHook[K][];
};
export const createNext = () => {
const walkHooks: WalkHookList = {};
const addHook = (walkHook: WalkHook) => {
Object.entries(walkHook).forEach(([key, value]) => {
const list = walkHooks[key as WalkHookKeys];
list ? list.push(value) : (walkHooks[key as WalkHookKeys] = [value]);
});
};
const runWalkHooks = (
key: T,
...args: Parameters[T]>
) => {
return walkHooks[key]?.map((hook) => hook!(...(args as [any, any])));
};
const next = (fiber: Fiber, skipChild = false): Fiber | undefined => {
if (runWalkHooks("enter", fiber, skipChild)?.includes(false)) return;
const { child } = fiber;
let nextFiber: Fiber | undefined = fiber;
if (child && !skipChild) {
runWalkHooks("down", nextFiber, child);
nextFiber = child;
} else {
while (nextFiber) {
const { sibling, parent } = nextFiber as Fiber;
if (sibling) {
runWalkHooks("sibling", nextFiber, sibling);
nextFiber = sibling;
break;
}
if (runWalkHooks("up", nextFiber, parent)?.includes(false)) {
nextFiber = undefined;
break;
}
nextFiber = parent;
}
}
runWalkHooks("return", nextFiber);
return nextFiber;
};
return [next, addHook] as const;
};
export const graft = (newFiber: Fiber, oldFiber: Fiber) => {
const parent = newFiber.parent!;
const parentChildren = parent.children!;
const index = newFiber.index!;
const preIndex = index - 1;
if (index === 0) parent.child = oldFiber;
if (preIndex >= 0) parentChildren[preIndex].sibling = oldFiber;
parentChildren[index] = oldFiber;
oldFiber.sibling = newFiber.sibling;
oldFiber.parent = parent;
};
export const findEls = (fiber: Fiber, findInPortal = false) => {
const els: FiberEl[] = [];
isElement(fiber)
? els.push(fiber.el!)
: isPortal(fiber) && !findInPortal
? false
: fiber.children?.forEach((child) => {
els.push(...findEls(child, findInPortal));
});
return els;
};
export const findLastElFiber = (fiber: Fiber): Fiber | undefined => {
if (isElement(fiber)) {
return fiber;
} else if (isPortal(fiber)) {
return undefined;
} else {
for (let i = 0; i < (fiber.children?.length ?? 0); i++) {
return findLastElFiber(fiber.children!.at(-(i + 1))!);
}
}
};
export type ContainerElement = Exclude;
export const getContainerElFiber = (
fiber: Fiber | undefined
): Fiber | undefined => {
while ((fiber = fiber?.parent)) {
if (isPortal(fiber) || isElement(fiber)) return fiber;
}
};
export const findToRoot = (
fiber: Fiber | undefined,
cb: (fiber: Fiber) => boolean
): Fiber | undefined => {
while ((fiber = fiber?.parent)) {
if (cb(fiber)) return fiber;
}
};
export const findRoot = (fiber: Fiber) =>
findToRoot(fiber, (fiber) => !fiber.parent)!;
export const findRuntime = (fiber: Fiber) =>
fiber.reconcileState?.rootWorkingFiber
? fiber.reconcileState.rootWorkingFiber.runtime!
: findRoot(fiber).runtime!;
================================================
FILE: packages/unis-core/src/h.ts
================================================
import { isNum, isStr, keys, toArray } from "./utils";
import {
COMPONENT,
createFiber,
ELEMENT,
Fiber,
FiberEl,
PORTAL,
TEXT,
} from "./fiber";
export const h = (tag: any, props: any, ...children: any[]) => {
props = { ...props };
if (children.length === 1) props.children = children[0];
if (children.length > 1) props.children = children;
return createFiber({
tag,
type: isStr(tag) ? ELEMENT : COMPONENT,
props,
...tag.take,
});
};
export const h2 = (tag: any, props: any, key?: string | number) => {
if (key !== undefined) props.key = key;
return createFiber({
tag,
type: isStr(tag) ? ELEMENT : COMPONENT,
props,
...tag.take,
});
};
export const formatChildren = (children: any) => {
const formatedChildren: Fiber[] = [];
for (const child of toArray(children)) {
if ([null, false, true, undefined].includes(child)) {
continue;
} else {
Array.isArray(child)
? formatedChildren.push(...formatChildren(child))
: formatedChildren.push(
isStr(child) || isNum(child)
? createFiber({
type: TEXT,
props: { nodeValue: child },
})
: child
);
}
}
return formatedChildren;
};
export const createRoot = (element: any, container: FiberEl): Fiber => {
return {
tag: (container as any).tagName.toLocaleLowerCase(),
type: ELEMENT,
el: container,
index: 0,
props: {
children: toArray(element),
},
};
};
export const createPortal = (child: JSX.Element, container: FiberEl) =>
createFiber({
type: PORTAL,
props: { children: child },
to: container,
});
const defaultCompare = (newProps: any = {}, oldProps: any = {}) => {
const newKeys = keys(newProps);
const oldKeys = keys(oldProps);
if (newKeys.length !== oldKeys.length) return false;
return newKeys.every((key) => Object.is(newProps[key], oldProps[key]));
};
export const memo = <
T extends ((props: any) => JSX.Element) & { compare?: Function }
>(
child: T,
compare: Function = defaultCompare
) => {
child.compare = compare;
return child;
};
export const cloneElement = (
element: Fiber,
props = {},
...children: JSX.Element[]
) => h(element.tag, { ...props, ...props }, ...children);
export const createElement = h;
export const Fragment = (props: any) => props.children;
export const FGMT = Fragment;
================================================
FILE: packages/unis-core/src/index.ts
================================================
export * from "./h";
export * from "./fiber";
export * from "./context";
export * from "./reconcile";
export * from "./utils";
export * from "./diff";
export * from "./createTokTik";
export * from "./api/utils";
export * from "./api/use";
export * from "./api/useState";
export * from "./api/useContext";
export * from "./api/useProps";
export * from "./api/useReducer";
export * from "./api/useMemo";
export * from "./api/useEffect";
export * from "./api/useLayoutEffect";
export * from "./api/useId";
export * from "./api/useRef";
export type * from "../types/jsx";
export const unisFns = {
use: 1,
useState: 1,
useProps: 1,
useContext: 1,
useReducer: 2,
useMemo: 2,
};
================================================
FILE: packages/unis-core/src/reconcile.ts
================================================
import {
clearAndRunEffects,
clearEffects,
Effect,
EFFECT_TYPE,
effectDepsEqual,
runEffects,
runStateEffects,
} from "./api/utils";
import {
createNext,
Fiber,
FLAG,
ReconcileState,
isComponent,
createFiber,
matchFlag,
findRuntime,
isElement,
} from "./fiber";
import { commit } from "./commit";
import { preElFiberWalkHook } from "./reconcileWalkHooks/preElFiber";
import { effectWalkHook } from "./reconcileWalkHooks/effect";
import { formatChildren } from "./h";
import { isFun } from "./utils";
import { diff } from "./diff";
import { contextWalkHook } from "./reconcileWalkHooks/context";
import { cutMemorizeState } from "./api/useReducer";
let workingFiber: Fiber | undefined;
export const getWorkingFiber = () => workingFiber;
export const setWorkingFiber = (fiber: Fiber | undefined) =>
(workingFiber = fiber);
// reconcile walker
const [next, addHook] = createNext();
// preEl
addHook(preElFiberWalkHook);
// effect
addHook(effectWalkHook);
// context
addHook(contextWalkHook);
export const readyForWork = (rootCurrentFiber: Fiber, hydrate = false) => {
rootCurrentFiber.runtime!.toktik.addTok(() =>
performWork(rootCurrentFiber, hydrate)
);
};
const performWork = (rootCurrentFiber: Fiber, hydrate: boolean) => {
const rootWorkingFiber = createFiber({
index: rootCurrentFiber.index,
tag: rootCurrentFiber.tag,
type: rootCurrentFiber.type,
props: rootCurrentFiber.props,
alternate: rootCurrentFiber,
el: rootCurrentFiber.el,
runtime: rootCurrentFiber.runtime,
});
const initialReconcileState: ReconcileState = {
rootWorkingFiber,
dispatchEffectList: [],
commitList: [],
dependencyList: [],
workingPreElFiber: undefined,
hydrate,
hydrateEl: rootCurrentFiber.el,
};
rootWorkingFiber.reconcileState = initialReconcileState;
setWorkingFiber(rootWorkingFiber);
tickWork(rootWorkingFiber!);
};
const tickWork = (workingFiber: Fiber) => {
const { toktik } = findRuntime(workingFiber);
let iFiber: Fiber | undefined = workingFiber;
// work loop
while (iFiber && !toktik.shouldYield()) {
const isReuse = !!matchFlag(iFiber.commitFlag, FLAG.REUSE);
{
!isReuse && update(iFiber);
!isReuse && isElement(iFiber) && compose(iFiber);
complete(iFiber);
}
iFiber = next(iFiber, isReuse);
setWorkingFiber(iFiber);
}
if (iFiber) {
toktik.addTik(() => {
setWorkingFiber(iFiber);
tickWork(iFiber!);
});
} else {
const { reconcileState } = workingFiber;
// switch dispatch bind fiber
runEffects(reconcileState!.dispatchEffectList);
// commit
commit(reconcileState!);
// call component effects
callComponentEffects(reconcileState!);
// clear reconcileState
for (const prop of Object.keys(reconcileState!)) {
delete reconcileState![prop as keyof ReconcileState];
}
}
};
const callComponentEffects = (reconcileState: ReconcileState) => {
const { commitList, rootWorkingFiber } = reconcileState!;
const { toktik } = rootWorkingFiber.runtime!;
const triggeredLayoutEffects: Effect[] = [];
const tickEffects: Effect[] = [];
// clear and run layoutEffects
for (const fiber of commitList) {
if (!isComponent(fiber)) continue;
for (const effect of fiber.effects ?? []) {
if (effect.type === EFFECT_TYPE.TICK) {
tickEffects.push(effect);
} else {
const equal = effectDepsEqual(effect);
if (!equal) {
triggeredLayoutEffects.push(effect);
clearEffects([effect]);
}
}
}
}
// run triggered layout effects
runEffects(triggeredLayoutEffects);
// clear and run tick effects
toktik.addTik(() => clearAndRunEffects(tickEffects));
};
const update = (fiber: Fiber) => {
if (isComponent(fiber)) {
updateComponent(fiber);
} else {
updateHost(fiber);
}
};
const updateHost = (fiber: Fiber) => {
diff(fiber, fiber.alternate?.children, formatChildren(fiber.props.children));
};
const updateComponent = (fiber: Fiber) => {
if (!fiber.renderFn) {
fiber.renderFn = fiber.tag as Function;
let rendered = fiber.renderFn(fiber.props);
if (isFun(rendered)) {
fiber.renderFn = rendered;
rendered = fiber.renderFn!();
}
fiber.rendered = formatChildren(rendered);
} else {
runStateEffects(fiber);
if (matchFlag(fiber.commitFlag, FLAG.UPDATE)) {
fiber.rendered = formatChildren(fiber.renderFn(fiber.props));
} else {
/**
* this condition, means `fiber.alternate` is on childFlag marked chain, and `fiber.commitFlag` is undefined.
* diff will keep going on.
*/
}
}
cutMemorizeState(fiber);
diff(fiber, fiber.alternate?.children, fiber.rendered);
};
const compose = (fiber: Fiber) => {
const { hydrate, hydrateEl } = fiber.reconcileState!;
const { operator } = findRuntime(fiber);
if (hydrate && hydrateEl) {
if (!operator.matchElement(fiber, hydrateEl))
throw new Error("Hydrate failed!");
fiber.el = hydrateEl;
fiber.reconcileState!.hydrateEl = operator.nextElement(hydrateEl);
} else if (matchFlag(fiber.commitFlag, FLAG.CREATE) && !hydrate) {
fiber.el = operator.createElement(fiber);
fiber.attrDiff?.length && operator.updateElementProperties(fiber);
let iFiber: Fiber | undefined = fiber;
while ((iFiber = iFiber.parent)) {
if (!matchFlag(iFiber.commitFlag, FLAG.CREATE)) break;
if (isElement(iFiber)) {
operator.insertBefore(iFiber, fiber.el, null);
break;
}
}
}
};
const complete = (fiber: Fiber) => {
!fiber.commitFlag && (fiber.alternate = undefined);
};
================================================
FILE: packages/unis-core/src/reconcileWalkHooks/context.ts
================================================
import { createDependency, findDependency } from "../context";
import { createNext, Fiber, isProvider, WalkHook } from "../fiber";
import { markFiber } from "../api/utils";
export const contextWalkHook: WalkHook = {
down: (from: Fiber, to?: Fiber) => {
isProvider(from) &&
from.reconcileState!.dependencyList.push(createDependency(from));
},
up: (from: Fiber, to?: Fiber) => {
to && isProvider(to) && from.reconcileState!.dependencyList.pop();
},
enter: (enter: Fiber, skipChild: boolean) => {
if (
enter.alternate &&
isProvider(enter.alternate) &&
!Object.is(enter.alternate.props.value, enter.props.value)
) {
let alternate = enter.alternate;
let iFiber: Fiber | undefined = alternate;
const [next, addHook] = createNext();
addHook({ up: (from, to) => to !== alternate });
do {
findDependency(iFiber, enter) && markFiber(iFiber);
iFiber = next(
iFiber,
iFiber !== alternate && isProvider(iFiber) && iFiber.tag === enter.tag
);
} while (iFiber);
}
},
};
================================================
FILE: packages/unis-core/src/reconcileWalkHooks/effect.ts
================================================
import { Fiber, WalkHook } from "../fiber";
export const pushEffect = (fiber: Fiber) => {
fiber.reconcileState!.commitList.push(fiber);
};
export const effectWalkHook: WalkHook = {
up: (from, to) => {
!from.child && from.commitFlag && pushEffect(from);
to?.commitFlag && pushEffect(to);
},
sibling: (from) => {
!from.child && from.commitFlag && pushEffect(from);
},
};
================================================
FILE: packages/unis-core/src/reconcileWalkHooks/preElFiber.ts
================================================
import {
Fiber,
findLastElFiber,
FLAG,
isElement,
isPortal,
matchFlag,
WalkHook,
} from "../fiber";
const setWorkingPreElFiber = (
fiber: Fiber,
workingPreElFiber: Fiber | undefined
) => {
if (fiber.reconcileState)
fiber.reconcileState.workingPreElFiber = workingPreElFiber;
};
const setReuseFiberPreElFiber = (fiber: Fiber) => {
if (!matchFlag(fiber.commitFlag, FLAG.REUSE)) return;
const lastElFiber = findLastElFiber(fiber.alternate!);
lastElFiber && setWorkingPreElFiber(fiber, lastElFiber);
};
export const preElFiberWalkHook: WalkHook = {
down: (from: Fiber, to?: Fiber) => {
isElement(from) && setWorkingPreElFiber(from, undefined);
isPortal(from) && setWorkingPreElFiber(from, undefined);
},
up: (from: Fiber, to?: Fiber) => {
if (from && !from.child) {
isElement(from) && setWorkingPreElFiber(from, from);
}
if (to) {
isElement(to) && setWorkingPreElFiber(from, to);
isPortal(to) && setWorkingPreElFiber(from, to.preElFiber);
}
setReuseFiberPreElFiber(from);
},
sibling: (from: Fiber, to?: Fiber) => {
if (matchFlag(from.commitFlag, FLAG.REUSE)) {
setReuseFiberPreElFiber(from);
} else {
isElement(from) && setWorkingPreElFiber(from, from);
}
},
return: (retn?: Fiber) => {
if (retn && matchFlag(retn.commitFlag, FLAG.INSERT))
retn.preElFiber = retn.reconcileState!.workingPreElFiber;
},
};
================================================
FILE: packages/unis-core/src/svg.ts
================================================
// kebab svg attr keys
export const displayAttrs = [
"baselineShift",
"alignmentBaseline",
"clip",
"clipPath",
"clipRule",
"color",
"colorInterpolation",
"colorInterpolationFilters",
"colorProfile",
"colorRendering",
"cursor",
"direction",
"display",
"dominantBaseline",
"enableBackground",
"fill",
"fillOpacity",
"fillRule",
"filter",
"floodColor",
"floodOpacity",
"fontFamily",
"fontSize",
"fontSizeAdjust",
"fontStretch",
"fontStyle",
"fontVariant",
"fontWeight",
"glyphOrientationHorizontal",
"glyphOrientationVertical",
"imageRendering",
"kerning",
"letterSpacing",
"lightingColor",
"markerEnd",
"markerMid",
"markerStart",
"mask",
"opacity",
"overflow",
"pointerEvents",
"shapeRendering",
"stopColor",
"stopOpacity",
"stroke",
"strokeDasharray",
"strokeDashoffset",
"strokeLinecap",
"strokeLinejoin",
"strokeMiterlimit",
"strokeOpacity",
"strokeWidth",
"textAnchor",
"transform",
"textDecoration",
"textRendering",
"unicodeBidi",
"vectorEffect",
"visibility",
"wordSpacing",
"writingMode",
];
================================================
FILE: packages/unis-core/src/utils.ts
================================================
import type { CSArray, CSObject } from "../types/jsx";
import { displayAttrs } from "./svg";
export const keys = Object.keys;
export const type = (a: any) =>
Object.prototype.toString.bind(a)().slice(8, -1);
export const isFun = (a: any): a is Function => typeof a === "function";
export const isStr = (a: any): a is string => typeof a === "string";
export const isNum = (a: any): a is number => typeof a === "number";
export const isBool = (a: any): a is boolean => typeof a === "boolean";
export const isSymbol = (a: any): a is boolean => typeof a === "symbol";
export const isArray = Array.isArray;
export const isObject = (a: any): a is object => type(a) === "Object";
export const isNullish = (a: any): a is null | undefined => a == null;
export const isEvent = (a: string) => a.startsWith("on");
export const getEventName = (event: string) => {
const [, eventName, capture] = event.match(/^on(.*)(Capture)?$/)!;
return [eventName.toLowerCase(), !!capture] as const;
};
export const camel2kebab = (text: string) =>
text.replace(/([A-Z])/g, "-$1").toLowerCase();
export const toArray = (a: T) => (Array.isArray(a) ? a : [a]);
export const arraysEqual = (a: any, b: any) => {
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (var i = 0; i < a.length; ++i) {
if (!Object.is(a[i], b[i])) return false;
}
return true;
};
export const styleStr = (style: Partial) =>
keys(style)
.map(
(key) => `${camel2kebab(key)}: ${style[key as keyof CSSStyleDeclaration]}`
)
.join("; ") + ";";
export const svgKey = (key: string) => {
for (const str of ["xmlns", "xml", "xlink"]) {
if (key.startsWith(str)) return key.toLowerCase().replace(str, `${str}:`);
}
return displayAttrs.includes(key) ? camel2kebab(key) : key;
};
export const classes = (cs: CSArray | CSObject): string => {
const objectClasses = (objcs: Record) =>
keys(objcs)
.reduce((pre, cur) => pre + " " + (objcs[cur] ? cur : ""), "")
.trim();
const arrayClasses = (arrcs: CSArray) =>
arrcs
.reduce(
(pre: string, cur) =>
pre +
" " +
`${
isNum(cur) || isStr(cur)
? cur
: isObject(cur)
? objectClasses(cur)
: isArray(cur)
? classes(cur)
: ""
}`,
""
)
.trim();
return isArray(cs) ? arrayClasses(cs) : objectClasses(cs);
};
let overflow = "";
let count = 0;
export const generateId = () => {
if (count === Number.MAX_SAFE_INTEGER) {
overflow += count.toString(32);
count = 0;
}
return `${overflow}${(count++).toString(32)}`;
};
================================================
FILE: packages/unis-core/test/utils.test.ts
================================================
/**
* @vitest-environment jsdom
*/
import { expect, it } from "vitest";
import { classes, svgKey, styleStr } from "../src/utils";
it("classes", () => {
expect(classes(["a", "b", 1, ["c", { d: true }]])).toBe("a b 1 c d");
expect(classes({ a: true, b: undefined, c: null })).toBe("a");
expect(classes({ a: false, b: true, c: null })).toBe("b");
});
it("realSVGAttr", () => {
expect(svgKey("glyphOrientationVertical")).toBe("glyph-orientation-vertical");
});
it("style2String", () => {
expect(styleStr({ background: "yellow", fontSize: "14px" })).toBe(
"background: yellow; font-size: 14px;"
);
});
================================================
FILE: packages/unis-core/tsconfig.build.json
================================================
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["**/*.test.*"]
}
================================================
FILE: packages/unis-core/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "build"
},
"include": ["src", "test", "jsx-runtime", "types/jsx.d.ts"]
}
================================================
FILE: packages/unis-core/types/jsx.d.ts
================================================
// Note: this file is auto concatenated to the end of the bundled d.ts during
// build.
// This code is based on react definition in DefinitelyTyped published under the MIT license.
// Repository: https://github.com/DefinitelyTyped/DefinitelyTyped
// Path in the repository: types/react/index.d.ts
//
// Copyrights of original definition are:
// AssureSign
// Microsoft
// John Reilly
// Benoit Benezech
// Patricio Zavolinsky
// Digiguru
// Eric Anderson
// Dovydas Navickas
// Josh Rutherford
// Guilherme Hübner
// Ferdy Budhidharma
// Johann Rakotoharisoa
// Olivier Pascal
// Martin Hochel
// Frank Li
// Jessica Franco
// Saransh Kataria
// Kanitkorn Sujautra
// Sebastian Silbermann
/**
*
The MIT License (MIT)
Copyright (c) 2018-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
*
*/
import * as CSS from "csstype";
export interface FiberNode {
tag?: string | Function;
type?: Symbol;
props?: any;
}
export interface CSSProperties
extends CSS.Properties,
CSS.PropertiesHyphen {
/**
* The index signature was removed to enable closed typing for style
* using CSSType. You're able to use type assertion or module augmentation
* to add properties or an index signature of your own.
*
* For examples and more information, visit:
* https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
*/
}
type Booleanish = boolean | "true" | "false";
type Numberish = number | string;
// All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/
interface AriaAttributes {
/** Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application. */
"aria-activedescendant"?: string;
/** Indicates whether assistive technologies will present all, or only parts of, the changed region based on the change notifications defined by the aria-relevant attribute. */
"aria-atomic"?: Booleanish;
/**
* Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for an input and specifies how predictions would be
* presented if they are made.
*/
"aria-autocomplete"?: "none" | "inline" | "list" | "both";
/** Indicates an element is being modified and that assistive technologies MAY want to wait until the modifications are complete before exposing them to the user. */
"aria-busy"?: Booleanish;
/**
* Indicates the current "checked" state of checkboxes, radio buttons, and other widgets.
* @see aria-pressed @see aria-selected.
*/
"aria-checked"?: Booleanish | "mixed";
/**
* Defines the total number of columns in a table, grid, or treegrid.
* @see aria-colindex.
*/
"aria-colcount"?: Numberish;
/**
* Defines an element's column index or position with respect to the total number of columns within a table, grid, or treegrid.
* @see aria-colcount @see aria-colspan.
*/
"aria-colindex"?: Numberish;
/**
* Defines the number of columns spanned by a cell or gridcell within a table, grid, or treegrid.
* @see aria-colindex @see aria-rowspan.
*/
"aria-colspan"?: Numberish;
/**
* Identifies the element (or elements) whose contents or presence are controlled by the current element.
* @see aria-owns.
*/
"aria-controls"?: string;
/** Indicates the element that represents the current item within a container or set of related elements. */
"aria-current"?: Booleanish | "page" | "step" | "location" | "date" | "time";
/**
* Identifies the element (or elements) that describes the object.
* @see aria-labelledby
*/
"aria-describedby"?: string;
/**
* Identifies the element that provides a detailed, extended description for the object.
* @see aria-describedby.
*/
"aria-details"?: string;
/**
* Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable.
* @see aria-hidden @see aria-readonly.
*/
"aria-disabled"?: Booleanish;
/**
* Indicates what functions can be performed when a dragged object is released on the drop target.
* @deprecated in ARIA 1.1
*/
"aria-dropeffect"?: "none" | "copy" | "execute" | "link" | "move" | "popup";
/**
* Identifies the element that provides an error message for the object.
* @see aria-invalid @see aria-describedby.
*/
"aria-errormessage"?: string;
/** Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. */
"aria-expanded"?: Booleanish;
/**
* Identifies the next element (or elements) in an alternate reading order of content which, at the user's discretion,
* allows assistive technology to override the general default of reading in document source order.
*/
"aria-flowto"?: string;
/**
* Indicates an element's "grabbed" state in a drag-and-drop operation.
* @deprecated in ARIA 1.1
*/
"aria-grabbed"?: Booleanish;
/** Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. */
"aria-haspopup"?:
| Booleanish
| "menu"
| "listbox"
| "tree"
| "grid"
| "dialog";
/**
* Indicates whether the element is exposed to an accessibility API.
* @see aria-disabled.
*/
"aria-hidden"?: Booleanish;
/**
* Indicates the entered value does not conform to the format expected by the application.
* @see aria-errormessage.
*/
"aria-invalid"?: Booleanish | "grammar" | "spelling";
/** Indicates keyboard shortcuts that an author has implemented to activate or give focus to an element. */
"aria-keyshortcuts"?: string;
/**
* Defines a string value that labels the current element.
* @see aria-labelledby.
*/
"aria-label"?: string;
/**
* Identifies the element (or elements) that labels the current element.
* @see aria-describedby.
*/
"aria-labelledby"?: string;
/** Defines the hierarchical level of an element within a structure. */
"aria-level"?: Numberish;
/** Indicates that an element will be updated, and describes the types of updates the user agents, assistive technologies, and user can expect from the live region. */
"aria-live"?: "off" | "assertive" | "polite";
/** Indicates whether an element is modal when displayed. */
"aria-modal"?: Booleanish;
/** Indicates whether a text box accepts multiple lines of input or only a single line. */
"aria-multiline"?: Booleanish;
/** Indicates that the user may select more than one item from the current selectable descendants. */
"aria-multiselectable"?: Booleanish;
/** Indicates whether the element's orientation is horizontal, vertical, or unknown/ambiguous. */
"aria-orientation"?: "horizontal" | "vertical";
/**
* Identifies an element (or elements) in order to define a visual, functional, or contextual parent/child relationship
* between DOM elements where the DOM hierarchy cannot be used to represent the relationship.
* @see aria-controls.
*/
"aria-owns"?: string;
/**
* Defines a short hint (a word or short phrase) intended to aid the user with data entry when the control has no value.
* A hint could be a sample value or a brief description of the expected format.
*/
"aria-placeholder"?: string;
/**
* Defines an element's number or position in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.
* @see aria-setsize.
*/
"aria-posinset"?: Numberish;
/**
* Indicates the current "pressed" state of toggle buttons.
* @see aria-checked @see aria-selected.
*/
"aria-pressed"?: Booleanish | "mixed";
/**
* Indicates that the element is not editable, but is otherwise operable.
* @see aria-disabled.
*/
"aria-readonly"?: Booleanish;
/**
* Indicates what notifications the user agent will trigger when the accessibility tree within a live region is modified.
* @see aria-atomic.
*/
"aria-relevant"?:
| "additions"
| "additions text"
| "all"
| "removals"
| "text";
/** Indicates that user input is required on the element before a form may be submitted. */
"aria-required"?: Booleanish;
/** Defines a human-readable, author-localized description for the role of an element. */
"aria-roledescription"?: string;
/**
* Defines the total number of rows in a table, grid, or treegrid.
* @see aria-rowindex.
*/
"aria-rowcount"?: Numberish;
/**
* Defines an element's row index or position with respect to the total number of rows within a table, grid, or treegrid.
* @see aria-rowcount @see aria-rowspan.
*/
"aria-rowindex"?: Numberish;
/**
* Defines the number of rows spanned by a cell or gridcell within a table, grid, or treegrid.
* @see aria-rowindex @see aria-colspan.
*/
"aria-rowspan"?: Numberish;
/**
* Indicates the current "selected" state of various widgets.
* @see aria-checked @see aria-pressed.
*/
"aria-selected"?: Booleanish;
/**
* Defines the number of items in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.
* @see aria-posinset.
*/
"aria-setsize"?: Numberish;
/** Indicates if items in a table or grid are sorted in ascending or descending order. */
"aria-sort"?: "none" | "ascending" | "descending" | "other";
/** Defines the maximum allowed value for a range widget. */
"aria-valuemax"?: Numberish;
/** Defines the minimum allowed value for a range widget. */
"aria-valuemin"?: Numberish;
/**
* Defines the current value for a range widget.
* @see aria-valuetext.
*/
"aria-valuenow"?: Numberish;
/** Defines the human readable text alternative of aria-valuenow for a range widget. */
"aria-valuetext"?: string;
}
// All the WAI-ARIA 1.1 role attribute values from https://www.w3.org/TR/wai-aria-1.1/#role_definitions
type AriaRole =
| "alert"
| "alertdialog"
| "application"
| "article"
| "banner"
| "button"
| "cell"
| "checkbox"
| "columnheader"
| "combobox"
| "complementary"
| "contentinfo"
| "definition"
| "dialog"
| "directory"
| "document"
| "feed"
| "figure"
| "form"
| "grid"
| "gridcell"
| "group"
| "heading"
| "img"
| "link"
| "list"
| "listbox"
| "listitem"
| "log"
| "main"
| "marquee"
| "math"
| "menu"
| "menubar"
| "menuitem"
| "menuitemcheckbox"
| "menuitemradio"
| "navigation"
| "none"
| "note"
| "option"
| "presentation"
| "progressbar"
| "radio"
| "radiogroup"
| "region"
| "row"
| "rowgroup"
| "rowheader"
| "scrollbar"
| "search"
| "searchbox"
| "separator"
| "slider"
| "spinbutton"
| "status"
| "switch"
| "tab"
| "table"
| "tablist"
| "tabpanel"
| "term"
| "textbox"
| "timer"
| "toolbar"
| "tooltip"
| "tree"
| "treegrid"
| "treeitem"
| (string & {});
// Vue's style normalization supports nested arrays
export type StyleValue = string | CSSProperties | Array;
export type CSValue = string | number | boolean | undefined | null;
export type CSObject = Record;
export type CSArray = (CSValue | CSObject | CSArray)[];
export interface HTMLAttributes extends AriaAttributes, EventHandlers {
// Standard HTML Attributes
accessKey?: string | undefined;
className?: string | CSObject | CSArray | undefined;
contentEditable?: Booleanish | "inherit" | undefined;
contextMenu?: string | undefined;
dir?: string | undefined;
draggable?: Booleanish | undefined;
hidden?: boolean | undefined;
id?: string | undefined;
lang?: string | undefined;
placeholder?: string | undefined;
slot?: string | undefined;
spellCheck?: Booleanish | undefined;
style?: CSSProperties | string | undefined;
tabIndex?: number | undefined;
title?: string | undefined;
translate?: "yes" | "no" | undefined;
// Unknown
radioGroup?: string | undefined; // ,
// WAI-ARIA
role?: AriaRole | undefined;
// RDFa Attributes
about?: string | undefined;
datatype?: string | undefined;
inlist?: any;
prefix?: string | undefined;
property?: string | undefined;
resource?: string | undefined;
typeof?: string | undefined;
vocab?: string | undefined;
// Non-standard Attributes
autoCapitalize?: string | undefined;
autoCorrect?: string | undefined;
autoSave?: string | undefined;
color?: string | undefined;
itemProp?: string | undefined;
itemScope?: boolean | undefined;
itemType?: string | undefined;
itemID?: string | undefined;
itemRef?: string | undefined;
results?: number | undefined;
security?: string | undefined;
unselectable?: "on" | "off" | undefined;
// Living Standard
/**
* Hints at the type of data that might be entered by the user while editing the element or its contents
* @see https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-inputmode-attribute
*/
inputMode?:
| "none"
| "text"
| "tel"
| "url"
| "email"
| "numeric"
| "decimal"
| "search"
| undefined;
/**
* Specify that a standard HTML element should behave like a defined custom built-in element
* @see https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is
*/
is?: string | undefined;
}
type HTMLAttributeReferrerPolicy =
| ""
| "no-referrer"
| "no-referrer-when-downgrade"
| "origin"
| "origin-when-cross-origin"
| "same-origin"
| "strict-origin"
| "strict-origin-when-cross-origin"
| "unsafe-url";
type HTMLAttributeAnchorTarget =
| "_self"
| "_blank"
| "_parent"
| "_top"
| (string & {});
export interface AnchorHTMLAttributes extends HTMLAttributes {
download?: any;
href?: string | undefined;
hrefLang?: string | undefined;
media?: string | undefined;
ping?: string | undefined;
rel?: string | undefined;
target?: HTMLAttributeAnchorTarget | undefined;
type?: string | undefined;
referrerPolicy?: HTMLAttributeReferrerPolicy | undefined;
}
export interface AudioHTMLAttributes extends MediaHTMLAttributes {}
export interface AreaHTMLAttributes extends HTMLAttributes {
alt?: string | undefined;
coords?: string | undefined;
download?: any;
href?: string | undefined;
hrefLang?: string | undefined;
media?: string | undefined;
referrerPolicy?: HTMLAttributeReferrerPolicy | undefined;
rel?: string | undefined;
shape?: string | undefined;
target?: string | undefined;
}
export interface BaseHTMLAttributes extends HTMLAttributes {
href?: string | undefined;
target?: string | undefined;
}
export interface BlockquoteHTMLAttributes extends HTMLAttributes {
cite?: string | undefined;
}
export interface ButtonHTMLAttributes extends HTMLAttributes {
autoFocus?: boolean | undefined;
disabled?: boolean | undefined;
form?: string | undefined;
formAction?: string | undefined;
formEncType?: string | undefined;
formMethod?: string | undefined;
formNoValidate?: boolean | undefined;
formTarget?: string | undefined;
name?: string | undefined;
type?: "submit" | "reset" | "button" | undefined;
value?: string | ReadonlyArray | number | undefined;
}
export interface CanvasHTMLAttributes extends HTMLAttributes {
height?: number | string | undefined;
width?: number | string | undefined;
}
export interface ColHTMLAttributes extends HTMLAttributes {
span?: number | undefined;
width?: number | string | undefined;
}
export interface ColgroupHTMLAttributes extends HTMLAttributes {
span?: number | undefined;
}
export interface DataHTMLAttributes extends HTMLAttributes {
value?: string | ReadonlyArray | number | undefined;
}
export interface DetailsHTMLAttributes extends HTMLAttributes {
open?: boolean | undefined;
}
export interface DelHTMLAttributes extends HTMLAttributes {
cite?: string | undefined;
dateTime?: string | undefined;
}
export interface DialogHTMLAttributes extends HTMLAttributes {
open?: boolean | undefined;
}
export interface EmbedHTMLAttributes extends HTMLAttributes {
height?: number | string | undefined;
src?: string | undefined;
type?: string | undefined;
width?: number | string | undefined;
}
export interface FieldsetHTMLAttributes extends HTMLAttributes {
disabled?: boolean | undefined;
form?: string | undefined;
name?: string | undefined;
}
export interface FormHTMLAttributes extends HTMLAttributes {
acceptCharset?: string | undefined;
action?: string | undefined;
autoComplete?: string | undefined;
encType?: string | undefined;
method?: string | undefined;
name?: string | undefined;
noValidate?: boolean | undefined;
target?: string | undefined;
}
export interface HtmlHTMLAttributes extends HTMLAttributes {
manifest?: string | undefined;
}
export interface IframeHTMLAttributes extends HTMLAttributes {
allow?: string | undefined;
allowFullScreen?: boolean | undefined;
allowTransparency?: boolean | undefined;
/** @deprecated */
frameBorder?: number | string | undefined;
height?: number | string | undefined;
loading?: "eager" | "lazy" | undefined;
/** @deprecated */
marginHeight?: number | undefined;
/** @deprecated */
marginWidth?: number | undefined;
name?: string | undefined;
referrerPolicy?: HTMLAttributeReferrerPolicy | undefined;
sandbox?: string | undefined;
/** @deprecated */
scrolling?: string | undefined;
seamless?: boolean | undefined;
src?: string | undefined;
srcDoc?: string | undefined;
width?: number | string | undefined;
}
export interface ImgHTMLAttributes extends HTMLAttributes {
alt?: string | undefined;
crossOrigin?: "anonymous" | "use-credentials" | "" | undefined;
decoding?: "async" | "auto" | "sync" | undefined;
height?: number | string | undefined;
loading?: "eager" | "lazy" | undefined;
referrerPolicy?: HTMLAttributeReferrerPolicy | undefined;
sizes?: string | undefined;
src?: string | undefined;
srcSet?: string | undefined;
useMap?: string | undefined;
width?: number | string | undefined;
}
export interface InsHTMLAttributes extends HTMLAttributes {
cite?: string | undefined;
dateTime?: string | undefined;
}
type HTMLInputTypeAttribute =
| "button"
| "checkbox"
| "color"
| "date"
| "datetime-local"
| "email"
| "file"
| "hidden"
| "image"
| "month"
| "number"
| "password"
| "radio"
| "range"
| "reset"
| "search"
| "submit"
| "tel"
| "text"
| "time"
| "url"
| "week"
| (string & {});
export interface InputHTMLAttributes extends HTMLAttributes {
accept?: string | undefined;
alt?: string | undefined;
autoComplete?: string | undefined;
autoFocus?: boolean | undefined;
capture?: boolean | "user" | "environment" | undefined; // https://www.w3.org/TR/html-media-capture/#the-capture-attribute
checked?: boolean | undefined;
crossOrigin?: string | undefined;
disabled?: boolean | undefined;
enterKeyHint?:
| "enter"
| "done"
| "go"
| "next"
| "previous"
| "search"
| "send"
| undefined;
form?: string | undefined;
formAction?: string | undefined;
formEncType?: string | undefined;
formMethod?: string | undefined;
formNoValidate?: boolean | undefined;
formTarget?: string | undefined;
height?: number | string | undefined;
list?: string | undefined;
max?: number | string | undefined;
maxLength?: number | undefined;
min?: number | string | undefined;
minLength?: number | undefined;
multiple?: boolean | undefined;
name?: string | undefined;
pattern?: string | undefined;
placeholder?: string | undefined;
readOnly?: boolean | undefined;
required?: boolean | undefined;
size?: number | undefined;
src?: string | undefined;
step?: number | string | undefined;
type?: HTMLInputTypeAttribute | undefined;
value?: string | ReadonlyArray | number | undefined;
width?: number | string | undefined;
}
export interface KeygenHTMLAttributes extends HTMLAttributes {
autoFocus?: boolean | undefined;
challenge?: string | undefined;
disabled?: boolean | undefined;
form?: string | undefined;
keyType?: string | undefined;
keyParams?: string | undefined;
name?: string | undefined;
}
export interface LabelHTMLAttributes extends HTMLAttributes {
form?: string | undefined;
htmlFor?: string | undefined;
}
export interface LiHTMLAttributes extends HTMLAttributes {
value?: string | ReadonlyArray | number | undefined;
}
export interface LinkHTMLAttributes extends HTMLAttributes {
as?: string | undefined;
crossOrigin?: string | undefined;
href?: string | undefined;
hrefLang?: string | undefined;
integrity?: string | undefined;
media?: string | undefined;
imageSrcSet?: string | undefined;
referrerPolicy?: HTMLAttributeReferrerPolicy | undefined;
rel?: string | undefined;
sizes?: string | undefined;
type?: string | undefined;
charSet?: string | undefined;
}
export interface MapHTMLAttributes extends HTMLAttributes {
name?: string | undefined;
}
export interface MenuHTMLAttributes extends HTMLAttributes {
type?: string | undefined;
}
export interface MediaHTMLAttributes extends HTMLAttributes {
autoPlay?: boolean | undefined;
controls?: boolean | undefined;
controlsList?: string | undefined;
crossOrigin?: string | undefined;
loop?: boolean | undefined;
mediaGroup?: string | undefined;
muted?: boolean | undefined;
playsInline?: boolean | undefined;
preload?: string | undefined;
src?: string | undefined;
}
export interface MetaHTMLAttributes extends HTMLAttributes {
charSet?: string | undefined;
content?: string | undefined;
httpEquiv?: string | undefined;
name?: string | undefined;
media?: string | undefined;
}
export interface MeterHTMLAttributes extends HTMLAttributes {
form?: string | undefined;
high?: number | undefined;
low?: number | undefined;
max?: number | string | undefined;
min?: number | string | undefined;
optimum?: number | undefined;
value?: string | ReadonlyArray | number | undefined;
}
export interface QuoteHTMLAttributes extends HTMLAttributes {
cite?: string | undefined;
}
export interface ObjectHTMLAttributes extends HTMLAttributes {
classID?: string | undefined;
data?: string | undefined;
form?: string | undefined;
height?: number | string | undefined;
name?: string | undefined;
type?: string | undefined;
useMap?: string | undefined;
width?: number | string | undefined;
wmode?: string | undefined;
}
export interface OlHTMLAttributes extends HTMLAttributes {
reversed?: boolean | undefined;
start?: number | undefined;
type?: "1" | "a" | "A" | "i" | "I" | undefined;
}
export interface OptgroupHTMLAttributes extends HTMLAttributes {
disabled?: boolean | undefined;
label?: string | undefined;
}
export interface OptionHTMLAttributes extends HTMLAttributes {
disabled?: boolean | undefined;
label?: string | undefined;
selected?: boolean | undefined;
value?: string | ReadonlyArray | number | undefined;
}
export interface OutputHTMLAttributes extends HTMLAttributes {
form?: string | undefined;
htmlFor?: string | undefined;
name?: string | undefined;
}
export interface ParamHTMLAttributes extends HTMLAttributes {
name?: string | undefined;
value?: string | ReadonlyArray | number | undefined;
}
export interface ProgressHTMLAttributes extends HTMLAttributes {
max?: number | string | undefined;
value?: string | ReadonlyArray | number | undefined;
}
export interface SlotHTMLAttributes extends HTMLAttributes {
name?: string | undefined;
}
export interface ScriptHTMLAttributes extends HTMLAttributes {
async?: boolean | undefined;
/** @deprecated */
charSet?: string | undefined;
crossOrigin?: string | undefined;
defer?: boolean | undefined;
integrity?: string | undefined;
noModule?: boolean | undefined;
nonce?: string | undefined;
referrerPolicy?: HTMLAttributeReferrerPolicy | undefined;
src?: string | undefined;
type?: string | undefined;
}
export interface SelectHTMLAttributes extends HTMLAttributes {
autoComplete?: string | undefined;
autoFocus?: boolean | undefined;
disabled?: boolean | undefined;
form?: string | undefined;
multiple?: boolean | undefined;
name?: string | undefined;
required?: boolean | undefined;
size?: number | undefined;
value?: string | ReadonlyArray | number | undefined;
}
export interface SourceHTMLAttributes extends HTMLAttributes {
height?: number | string | undefined;
media?: string | undefined;
sizes?: string | undefined;
src?: string | undefined;
srcSet?: string | undefined;
type?: string | undefined;
width?: number | string | undefined;
}
export interface StyleHTMLAttributes extends HTMLAttributes {
media?: string | undefined;
nonce?: string | undefined;
scoped?: boolean | undefined;
type?: string | undefined;
}
export interface TableHTMLAttributes extends HTMLAttributes {
cellPadding?: number | string | undefined;
cellSpacing?: number | string | undefined;
summary?: string | undefined;
width?: number | string | undefined;
}
export interface TextareaHTMLAttributes extends HTMLAttributes {
autoComplete?: string | undefined;
autoFocus?: boolean | undefined;
cols?: number | undefined;
dirName?: string | undefined;
disabled?: boolean | undefined;
form?: string | undefined;
maxLength?: number | undefined;
minLength?: number | undefined;
name?: string | undefined;
placeholder?: string | undefined;
readOnly?: boolean | undefined;
required?: boolean | undefined;
rows?: number | undefined;
value?: string | ReadonlyArray | number | undefined;
wrap?: string | undefined;
}
export interface TdHTMLAttributes extends HTMLAttributes {
align?: "left" | "center" | "right" | "justify" | "char" | undefined;
colSpan?: number | undefined;
headers?: string | undefined;
rowSpan?: number | undefined;
scope?: string | undefined;
abbr?: string | undefined;
height?: number | string | undefined;
width?: number | string | undefined;
valign?: "top" | "middle" | "bottom" | "baseline" | undefined;
}
export interface ThHTMLAttributes extends HTMLAttributes {
align?: "left" | "center" | "right" | "justify" | "char" | undefined;
colSpan?: number | undefined;
headers?: string | undefined;
rowSpan?: number | undefined;
scope?: string | undefined;
abbr?: string | undefined;
}
export interface TimeHTMLAttributes extends HTMLAttributes {
dateTime?: string | undefined;
}
export interface TrackHTMLAttributes extends HTMLAttributes {
default?: boolean | undefined;
kind?: string | undefined;
label?: string | undefined;
src?: string | undefined;
srcLang?: string | undefined;
}
export interface VideoHTMLAttributes extends MediaHTMLAttributes {
height?: number | string | undefined;
playsInline?: boolean | undefined;
poster?: string | undefined;
width?: number | string | undefined;
disablePictureInPicture?: boolean | undefined;
disableRemotePlayback?: boolean | undefined;
}
export interface WebViewHTMLAttributes extends HTMLAttributes {
allowFullScreen?: boolean | undefined;
allowpopups?: boolean | undefined;
autoFocus?: boolean | undefined;
autosize?: boolean | undefined;
blinkfeatures?: string | undefined;
disableblinkfeatures?: string | undefined;
disableguestresize?: boolean | undefined;
disablewebsecurity?: boolean | undefined;
guestinstance?: string | undefined;
httpreferrer?: string | undefined;
nodeintegration?: boolean | undefined;
partition?: string | undefined;
plugins?: boolean | undefined;
preload?: string | undefined;
src?: string | undefined;
useragent?: string | undefined;
webpreferences?: string | undefined;
}
export interface SVGAttributes extends AriaAttributes, EventHandlers {
innerHTML?: string;
/**
* SVG Styling Attributes
* @see https://www.w3.org/TR/SVG/styling.html#ElementSpecificStyling
*/
class?: any;
className?: string | CSObject | CSArray | undefined;
color?: string | undefined;
height?: number | string | undefined;
id?: string | undefined;
lang?: string | undefined;
max?: number | string | undefined;
media?: string | undefined;
method?: string | undefined;
min?: number | string | undefined;
name?: string | undefined;
style?: CSSProperties | string | undefined;
target?: string | undefined;
type?: string | undefined;
width?: number | string | undefined;
// Other HTML properties supported by SVG elements in browsers
role?: AriaRole | undefined;
tabIndex?: number | undefined;
crossOrigin?: "anonymous" | "use-credentials" | "" | undefined;
// SVG Specific attributes
accentHeight?: number | string | undefined;
accumulate?: "none" | "sum" | undefined;
additive?: "replace" | "sum" | undefined;
alignmentBaseline?:
| "auto"
| "baseline"
| "before-edge"
| "text-before-edge"
| "middle"
| "central"
| "after-edge"
| "text-after-edge"
| "ideographic"
| "alphabetic"
| "hanging"
| "mathematical"
| "inherit"
| undefined;
allowReorder?: "no" | "yes" | undefined;
alphabetic?: number | string | undefined;
amplitude?: number | string | undefined;
arabicForm?: "initial" | "medial" | "terminal" | "isolated" | undefined;
ascent?: number | string | undefined;
attributeName?: string | undefined;
attributeType?: string | undefined;
autoReverse?: Booleanish | undefined;
azimuth?: number | string | undefined;
baseFrequency?: number | string | undefined;
baselineShift?: number | string | undefined;
baseProfile?: number | string | undefined;
bbox?: number | string | undefined;
begin?: number | string | undefined;
bias?: number | string | undefined;
by?: number | string | undefined;
calcMode?: number | string | undefined;
capHeight?: number | string | undefined;
clip?: number | string | undefined;
clipPath?: string | undefined;
clipPathUnits?: number | string | undefined;
clipRule?: number | string | undefined;
colorInterpolation?: number | string | undefined;
colorInterpolationFilters?:
| "auto"
| "sRGB"
| "linearRGB"
| "inherit"
| undefined;
colorProfile?: number | string | undefined;
colorRendering?: number | string | undefined;
contentScriptType?: number | string | undefined;
contentStyleType?: number | string | undefined;
cursor?: number | string | undefined;
cx?: number | string | undefined;
cy?: number | string | undefined;
d?: string | undefined;
decelerate?: number | string | undefined;
descent?: number | string | undefined;
diffuseConstant?: number | string | undefined;
direction?: number | string | undefined;
display?: number | string | undefined;
divisor?: number | string | undefined;
dominantBaseline?: number | string | undefined;
dur?: number | string | undefined;
dx?: number | string | undefined;
dy?: number | string | undefined;
edgeMode?: number | string | undefined;
elevation?: number | string | undefined;
enableBackground?: number | string | undefined;
end?: number | string | undefined;
exponent?: number | string | undefined;
externalResourcesRequired?: Booleanish | undefined;
fill?: string | undefined;
fillOpacity?: number | string | undefined;
fillRule?: "nonzero" | "evenodd" | "inherit" | undefined;
filter?: string | undefined;
filterRes?: number | string | undefined;
filterUnits?: number | string | undefined;
floodColor?: number | string | undefined;
floodOpacity?: number | string | undefined;
focusable?: Booleanish | "auto" | undefined;
fontFamily?: string | undefined;
fontSize?: number | string | undefined;
fontSizeAdjust?: number | string | undefined;
fontStretch?: number | string | undefined;
fontStyle?: number | string | undefined;
fontVariant?: number | string | undefined;
fontWeight?: number | string | undefined;
format?: number | string | undefined;
fr?: number | string | undefined;
from?: number | string | undefined;
fx?: number | string | undefined;
fy?: number | string | undefined;
g1?: number | string | undefined;
g2?: number | string | undefined;
glyphName?: number | string | undefined;
glyphOrientationHorizontal?: number | string | undefined;
glyphOrientationVertical?: number | string | undefined;
glyphRef?: number | string | undefined;
gradientTransform?: string | undefined;
gradientUnits?: string | undefined;
hanging?: number | string | undefined;
horizAdvX?: number | string | undefined;
horizOriginX?: number | string | undefined;
href?: string | undefined;
ideographic?: number | string | undefined;
imageRendering?: number | string | undefined;
in2?: number | string | undefined;
in?: string | undefined;
intercept?: number | string | undefined;
k1?: number | string | undefined;
k2?: number | string | undefined;
k3?: number | string | undefined;
k4?: number | string | undefined;
k?: number | string | undefined;
kernelMatrix?: number | string | undefined;
kernelUnitLength?: number | string | undefined;
kerning?: number | string | undefined;
keyPoints?: number | string | undefined;
keySplines?: number | string | undefined;
keyTimes?: number | string | undefined;
lengthAdjust?: number | string | undefined;
letterSpacing?: number | string | undefined;
lightingColor?: number | string | undefined;
limitingConeAngle?: number | string | undefined;
local?: number | string | undefined;
markerEnd?: string | undefined;
markerHeight?: number | string | undefined;
markerMid?: string | undefined;
markerStart?: string | undefined;
markerUnits?: number | string | undefined;
markerWidth?: number | string | undefined;
mask?: string | undefined;
maskContentUnits?: number | string | undefined;
maskUnits?: number | string | undefined;
mathematical?: number | string | undefined;
mode?: number | string | undefined;
numOctaves?: number | string | undefined;
offset?: number | string | undefined;
opacity?: number | string | undefined;
operator?: number | string | undefined;
order?: number | string | undefined;
orient?: number | string | undefined;
orientation?: number | string | undefined;
origin?: number | string | undefined;
overflow?: number | string | undefined;
overlinePosition?: number | string | undefined;
overlineThickness?: number | string | undefined;
paintOrder?: number | string | undefined;
panose1?: number | string | undefined;
path?: string | undefined;
pathLength?: number | string | undefined;
patternContentUnits?: string | undefined;
patternTransform?: number | string | undefined;
patternUnits?: string | undefined;
pointerEvents?: number | string | undefined;
points?: string | undefined;
pointsAtX?: number | string | undefined;
pointsAtY?: number | string | undefined;
pointsAtZ?: number | string | undefined;
preserveAlpha?: Booleanish | undefined;
preserveAspectRatio?: string | undefined;
primitiveUnits?: number | string | undefined;
r?: number | string | undefined;
radius?: number | string | undefined;
refX?: number | string | undefined;
refY?: number | string | undefined;
renderingIntent?: number | string | undefined;
repeatCount?: number | string | undefined;
repeatDur?: number | string | undefined;
requiredExtensions?: number | string | undefined;
requiredFeatures?: number | string | undefined;
restart?: number | string | undefined;
result?: string | undefined;
rotate?: number | string | undefined;
rx?: number | string | undefined;
ry?: number | string | undefined;
scale?: number | string | undefined;
seed?: number | string | undefined;
shapeRendering?: number | string | undefined;
slope?: number | string | undefined;
spacing?: number | string | undefined;
specularConstant?: number | string | undefined;
specularExponent?: number | string | undefined;
speed?: number | string | undefined;
spreadMethod?: string | undefined;
startOffset?: number | string | undefined;
stdDeviation?: number | string | undefined;
stemh?: number | string | undefined;
stemv?: number | string | undefined;
stitchTiles?: number | string | undefined;
stopColor?: string | undefined;
stopOpacity?: number | string | undefined;
strikethroughPosition?: number | string | undefined;
strikethroughThickness?: number | string | undefined;
string?: number | string | undefined;
stroke?: string | undefined;
strokeDasharray?: string | number | undefined;
strokeDashoffset?: string | number | undefined;
strokeLinecap?: "butt" | "round" | "square" | "inherit" | undefined;
strokeLinejoin?: "miter" | "round" | "bevel" | "inherit" | undefined;
strokeMiterlimit?: number | string | undefined;
strokeOpacity?: number | string | undefined;
strokeWidth?: number | string | undefined;
surfaceScale?: number | string | undefined;
systemLanguage?: number | string | undefined;
tableValues?: number | string | undefined;
targetX?: number | string | undefined;
targetY?: number | string | undefined;
textAnchor?: string | undefined;
textDecoration?: number | string | undefined;
textLength?: number | string | undefined;
textRendering?: number | string | undefined;
to?: number | string | undefined;
transform?: string | undefined;
u1?: number | string | undefined;
u2?: number | string | undefined;
underlinePosition?: number | string | undefined;
underlineThickness?: number | string | undefined;
unicode?: number | string | undefined;
unicodeBidi?: number | string | undefined;
unicodeRange?: number | string | undefined;
unitsPerEm?: number | string | undefined;
vAlphabetic?: number | string | undefined;
values?: string | undefined;
vectorEffect?: number | string | undefined;
version?: string | undefined;
vertAdvY?: number | string | undefined;
vertOriginX?: number | string | undefined;
vertOriginY?: number | string | undefined;
vHanging?: number | string | undefined;
vIdeographic?: number | string | undefined;
viewBox?: string | undefined;
viewTarget?: number | string | undefined;
visibility?: number | string | undefined;
vMathematical?: number | string | undefined;
widths?: number | string | undefined;
wordSpacing?: number | string | undefined;
writingMode?: number | string | undefined;
x1?: number | string | undefined;
x2?: number | string | undefined;
x?: number | string | undefined;
xChannelSelector?: string | undefined;
xHeight?: number | string | undefined;
xlinkActuate?: string | undefined;
xlinkArcrole?: string | undefined;
xlinkHref?: string | undefined;
xlinkRole?: string | undefined;
xlinkShow?: string | undefined;
xlinkTitle?: string | undefined;
xlinkType?: string | undefined;
xmlBase?: string | undefined;
xmlLang?: string | undefined;
xmlns?: string | undefined;
xmlnsXlink?: string | undefined;
xmlSpace?: string | undefined;
y1?: number | string | undefined;
y2?: number | string | undefined;
y?: number | string | undefined;
yChannelSelector?: string | undefined;
z?: number | string | undefined;
zoomAndPan?: string | undefined;
}
export interface IntrinsicElementAttributes {
a: AnchorHTMLAttributes;
abbr: HTMLAttributes;
address: HTMLAttributes;
area: AreaHTMLAttributes;
article: HTMLAttributes;
aside: HTMLAttributes;
audio: AudioHTMLAttributes;
b: HTMLAttributes;
base: BaseHTMLAttributes;
bdi: HTMLAttributes;
bdo: HTMLAttributes;
blockquote: BlockquoteHTMLAttributes;
body: HTMLAttributes;
br: HTMLAttributes;
button: ButtonHTMLAttributes;
canvas: CanvasHTMLAttributes;
caption: HTMLAttributes;
cite: HTMLAttributes;
code: HTMLAttributes;
col: ColHTMLAttributes;
colgroup: ColgroupHTMLAttributes;
data: DataHTMLAttributes;
datalist: HTMLAttributes;
dd: HTMLAttributes;
del: DelHTMLAttributes;
details: DetailsHTMLAttributes;
dfn: HTMLAttributes;
dialog: DialogHTMLAttributes;
div: HTMLAttributes;
dl: HTMLAttributes;
dt: HTMLAttributes;
em: HTMLAttributes;
embed: EmbedHTMLAttributes;
fieldset: FieldsetHTMLAttributes;
figcaption: HTMLAttributes;
figure: HTMLAttributes;
footer: HTMLAttributes;
form: FormHTMLAttributes;
h1: HTMLAttributes;
h2: HTMLAttributes;
h3: HTMLAttributes;
h4: HTMLAttributes;
h5: HTMLAttributes;
h6: HTMLAttributes;
head: HTMLAttributes;
header: HTMLAttributes;
hgroup: HTMLAttributes;
hr: HTMLAttributes;
html: HtmlHTMLAttributes;
i: HTMLAttributes;
iframe: IframeHTMLAttributes;
img: ImgHTMLAttributes;
input: InputHTMLAttributes;
ins: InsHTMLAttributes;
kbd: HTMLAttributes;
keygen: KeygenHTMLAttributes;
label: LabelHTMLAttributes;
legend: HTMLAttributes;
li: LiHTMLAttributes;
link: LinkHTMLAttributes;
main: HTMLAttributes;
map: MapHTMLAttributes;
mark: HTMLAttributes;
menu: MenuHTMLAttributes;
meta: MetaHTMLAttributes;
meter: MeterHTMLAttributes;
nav: HTMLAttributes;
noindex: HTMLAttributes;
noscript: HTMLAttributes;
object: ObjectHTMLAttributes;
ol: OlHTMLAttributes;
optgroup: OptgroupHTMLAttributes;
option: OptionHTMLAttributes;
output: OutputHTMLAttributes;
p: HTMLAttributes;
param: ParamHTMLAttributes;
picture: HTMLAttributes;
pre: HTMLAttributes;
progress: ProgressHTMLAttributes;
q: QuoteHTMLAttributes;
rp: HTMLAttributes;
rt: HTMLAttributes;
ruby: HTMLAttributes;
s: HTMLAttributes;
samp: HTMLAttributes;
script: ScriptHTMLAttributes;
section: HTMLAttributes;
select: SelectHTMLAttributes;
slot: SlotHTMLAttributes;
small: HTMLAttributes;
source: SourceHTMLAttributes;
span: HTMLAttributes;
strong: HTMLAttributes;
style: StyleHTMLAttributes;
sub: HTMLAttributes;
summary: HTMLAttributes;
sup: HTMLAttributes;
table: TableHTMLAttributes;
template: HTMLAttributes;
tbody: HTMLAttributes;
td: TdHTMLAttributes;
textarea: TextareaHTMLAttributes;
tfoot: HTMLAttributes;
th: ThHTMLAttributes;
thead: HTMLAttributes;
time: TimeHTMLAttributes;
title: HTMLAttributes;
tr: HTMLAttributes;
track: TrackHTMLAttributes;
u: HTMLAttributes;
ul: HTMLAttributes;
var: HTMLAttributes;
video: VideoHTMLAttributes;
wbr: HTMLAttributes;
webview: WebViewHTMLAttributes;
// SVG
svg: SVGAttributes;
animate: SVGAttributes;
animateMotion: SVGAttributes;
animateTransform: SVGAttributes;
circle: SVGAttributes;
clipPath: SVGAttributes;
defs: SVGAttributes;
desc: SVGAttributes;
ellipse: SVGAttributes;
feBlend: SVGAttributes;
feColorMatrix: SVGAttributes;
feComponentTransfer: SVGAttributes;
feComposite: SVGAttributes;
feConvolveMatrix: SVGAttributes;
feDiffuseLighting: SVGAttributes;
feDisplacementMap: SVGAttributes;
feDistantLight: SVGAttributes;
feDropShadow: SVGAttributes;
feFlood: SVGAttributes;
feFuncA: SVGAttributes;
feFuncB: SVGAttributes;
feFuncG: SVGAttributes;
feFuncR: SVGAttributes;
feGaussianBlur: SVGAttributes;
feImage: SVGAttributes;
feMerge: SVGAttributes;
feMergeNode: SVGAttributes;
feMorphology: SVGAttributes;
feOffset: SVGAttributes;
fePointLight: SVGAttributes;
feSpecularLighting: SVGAttributes;
feSpotLight: SVGAttributes;
feTile: SVGAttributes;
feTurbulence: SVGAttributes;
filter: SVGAttributes;
foreignObject: SVGAttributes;
g: SVGAttributes;
image: SVGAttributes;
line: SVGAttributes;
linearGradient: SVGAttributes;
marker: SVGAttributes;
mask: SVGAttributes;
metadata: SVGAttributes;
mpath: SVGAttributes;
path: SVGAttributes;
pattern: SVGAttributes;
polygon: SVGAttributes;
polyline: SVGAttributes;
radialGradient: SVGAttributes;
rect: SVGAttributes;
stop: SVGAttributes;
switch: SVGAttributes;
symbol: SVGAttributes;
text: SVGAttributes;
textPath: SVGAttributes;
tspan: SVGAttributes;
use: SVGAttributes;
view: SVGAttributes;
}
export interface Events {
// Clipboard Events
onCopy: ClipboardEvent;
onCut: ClipboardEvent;
onPaste: ClipboardEvent;
// Composition Events
onCompositionEnd: CompositionEvent;
onCompositionStart: CompositionEvent;
onCompositionUpdate: CompositionEvent;
// Focus Events
onFocus: FocusEvent;
onBlur: FocusEvent;
// Form Events
onChange: Event;
onBeforeInput: Event;
onInput: Event;
onReset: Event;
onSubmit: Event;
onInvalid: Event;
// Image Events
onLoad: Event;
onError: Event;
// Keyboard Events
onKeyDown: KeyboardEvent;
onKeyPress: KeyboardEvent;
onKeyUp: KeyboardEvent;
// Media Events
onAbort: Event;
onCanPlay: Event;
onCanPlayThrough: Event;
onDurationChange: Event;
onEmptied: Event;
onEncrypted: Event;
onEnded: Event;
onLoadedData: Event;
onLoadedMetadata: Event;
onLoadStart: Event;
onPause: Event;
onPlay: Event;
onPlaying: Event;
onProgress: Event;
onRateChange: Event;
onSeeked: Event;
onSeeking: Event;
onStalled: Event;
onSuspend: Event;
onTimeUpdate: Event;
onVolumeChange: Event;
onWaiting: Event;
// DragEvent
onDrag: DragEvent;
onDragEnd: DragEvent;
onDragEnter: DragEvent;
onDragExit: DragEvent;
onDragLeave: DragEvent;
onDragOver: DragEvent;
onDragStart: DragEvent;
onDrop: DragEvent;
// MouseEvents
onAuxClick: MouseEvent;
onClick: MouseEvent;
onContextMenu: MouseEvent;
onDoubleClick: MouseEvent;
onMouseDown: MouseEvent;
onMouseEnter: MouseEvent;
onMouseLeave: MouseEvent;
onMouseMove: MouseEvent;
onMouseOut: MouseEvent;
onMouseOver: MouseEvent;
onMouseUp: MouseEvent;
// Selection Events
onSelect: Event;
// Touch Events
onTouchCancel: TouchEvent;
onTouchEnd: TouchEvent;
onTouchMove: TouchEvent;
onTouchStart: TouchEvent;
// Pointer Events
onPointerDown: PointerEvent;
onPointerMove: PointerEvent;
onPointerUp: PointerEvent;
onPointerCancel: PointerEvent;
onPointerEnter: PointerEvent;
onPointerLeave: PointerEvent;
onPointerOver: PointerEvent;
onPointerOut: PointerEvent;
// UI Events
onScroll: UIEvent;
// Wheel Events
onWheel: WheelEvent;
// Animation Events
onAnimationStart: AnimationEvent;
onAnimationEnd: AnimationEvent;
onAnimationIteration: AnimationEvent;
// Transition Events
onTransitionEnd: TransitionEvent;
onTransitionStart: TransitionEvent;
}
type EventHandlers = {
[K in keyof E]?: E[K] extends Function ? E[K] : (payload: E[K]) => void;
};
type ReservedProps = {
children?: unknown;
key?: string | number | symbol;
ref?: { current: any } | ((ref: Element | null) => void);
};
export type ElementAttrs = T & ReservedProps;
type NativeElements = {
[K in keyof IntrinsicElementAttributes]: ElementAttrs<
IntrinsicElementAttributes[K]
>;
};
declare global {
namespace JSX {
type ElementTypes =
| string
| number
| null
| boolean
| void
| undefined
| FiberNode;
type Element =
| Element[]
| ElementTypes
| ElementTypes[]
| (() => Element)
| (() => Element[]);
interface ElementClass {
props: {};
}
interface ElementAttributesProperty {
props: {};
}
interface ElementChildrenAttribute {
children: {};
}
interface IntrinsicElements extends NativeElements {
// allow arbitrary elements
// @ts-ignore suppress ts:2374 = Duplicate string index signature.
[name: string]: any;
}
interface IntrinsicAttributes extends ReservedProps {}
}
}
// suppress ts:2669
export {};
================================================
FILE: packages/unis-core/vitest.config.ts
================================================
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
coverage: {
reporter: ["text", "json", "html"],
},
},
});
================================================
FILE: packages/unis-dom/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
build
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
================================================
FILE: packages/unis-dom/index.d.ts
================================================
import "@unis/core";
export * from "./dist/browser";
================================================
FILE: packages/unis-dom/package.json
================================================
{
"name": "@unis/dom",
"version": "1.2.5",
"description": "Unis is a simpler and easier to use front-end framework than React",
"main": "dist/browser.js",
"module": "dist/browser.mjs",
"types": "index.d.ts",
"typings": "index.d.ts",
"exports": {
".": {
"require": "./dist/browser.js",
"import": "./dist/browser.mjs"
},
"./server": {
"require": "./dist/server.js",
"import": "./dist/server.mjs"
}
},
"scripts": {
"build": "rimraf build && rimraf dist && tsc -p tsconfig.build.json && rollup --config",
"build:dev": "cross-env NODE_ENV=development pnpm build",
"build:server": "rollup --config rollup.config.server.mjs",
"test": "vitest run --coverage",
"test:watch": "vitest -w"
},
"repository": {
"type": "git",
"url": "git+https://github.com/anuoua/unis.git"
},
"keywords": [
"frontend",
"web",
"framwork"
],
"files": [
"dist",
"server.d.ts",
"index.d.ts"
],
"author": "anuoua",
"license": "MIT",
"bugs": {
"url": "https://github.com/anuoua/unis/issues"
},
"homepage": "https://github.com/anuoua/unis#readme",
"peerDependencies": {
"@unis/core": "workspace:^"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.2",
"@types/jsdom": "^21.1.1",
"@unis/core": "workspace:^",
"@unis/vite-preset": "workspace:^",
"@vitest/coverage-c8": "^0.28.5",
"cross-env": "^7.0.3",
"esbuild": "^0.17.15",
"jsdom": "^21.1.1",
"rimraf": "^4.4.1",
"rollup": "^3.20.2",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vite": "^4.2.1",
"vitest": "^0.29.8"
}
}
================================================
FILE: packages/unis-dom/rollup.config.mjs
================================================
import esbuild from "rollup-plugin-esbuild";
import dts from "rollup-plugin-dts";
import { defineConfig } from "rollup";
import { nodeResolve } from "@rollup/plugin-node-resolve";
const configGen = (format, plateform) =>
defineConfig({
input: `src/${plateform}/index.ts`,
external: [/^@unis/],
output: [
{
dir: "dist",
entryFileNames: `${plateform}.${format === "esm" ? "mjs" : "js"}`,
format,
sourcemap: true,
},
],
plugins: [
nodeResolve(),
esbuild({
sourceMap: true,
target: "esnext",
}),
],
});
const dtsRollup = (which) =>
defineConfig({
input: `build/${which}/index.d.ts`,
output: [{ file: `dist/${which}.d.ts`, format: "es" }],
plugins: [dts()],
});
const config = [
configGen("cjs", "browser"),
configGen("esm", "browser"),
configGen("cjs", "server"),
configGen("esm", "server"),
dtsRollup("browser"),
dtsRollup("server"),
];
export default config;
================================================
FILE: packages/unis-dom/server.d.ts
================================================
export * from "./dist/server";
================================================
FILE: packages/unis-dom/src/browser/__test__/context.test.tsx
================================================
/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, expect, it } from "vitest";
import { useContext } from "@unis/core";
import { useEffect } from "@unis/core";
import { useProps } from "@unis/core";
import { useState } from "@unis/core";
import { createContext } from "@unis/core";
import { Fragment, memo } from "@unis/core";
import { rendered, testRender } from "./util";
let root: Element;
beforeEach(() => {
root = document.createElement("div");
document.body.append(root);
});
afterEach(() => {
root.innerHTML = "";
});
it("context", async () => {
const AppContext = createContext("initial");
const Cpp = memo(() => {
let theme = useContext(AppContext);
return () => Cpp: {theme}
;
});
const Dpp = () => {
return () => (
{(theme) => Dpp: {theme}
}
);
};
const Epp = () => {
return () => (
{(theme) => Epp: {theme}
}
);
};
const Bpp = () => {
let theme = useContext(AppContext);
return () => (
);
};
const App = () => {
let [theme, setTheme] = useState("light");
useEffect(
() => {
setTheme("dark");
},
() => []
);
return () => (
App
);
};
testRender( , root);
expect(root.innerHTML).toBe(
"App
Dpp: light
Epp: initial
"
);
await rendered();
expect(root.innerHTML).toBe(
"App
Dpp: dark
Epp: initial
"
);
});
it("context pass through", async () => {
const AppContext = createContext({} as any);
const App = () => {
let [hello, setHello] = useState("hello");
return () => (
);
};
const Bpp = () => {
let { hello, setHello } = useContext(AppContext);
return () => ;
};
const Cpp = (p: { msg: string; setMsg: (msg: string) => void }) => {
let { msg, setMsg } = useProps(p);
let [count, setCount] = useState(0);
useEffect(
() => {
setMsg("world");
setCount(count + 1);
},
() => []
);
return () => msg;
};
testRender( , root);
expect(root.innerHTML).toBe("hello
");
await rendered();
expect(root.innerHTML).toBe("world
");
});
================================================
FILE: packages/unis-dom/src/browser/__test__/dom.test.tsx
================================================
/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, expect, it } from "vitest";
import { useEffect } from "@unis/core";
import { useState } from "@unis/core";
import { rendered, testRender } from "./util";
let root: Element;
beforeEach(() => {
root = document.createElement("div");
document.body.append(root);
});
afterEach(() => {
root.innerHTML = "";
});
it("dom", async () => {
const App = () => {
let [toggle, setToggle] = useState(true);
const getCurrentStyle = () => {
return toggle
? {
style: {
background: "yellow",
},
tabindex: "1",
className: "class1",
onClick: () => {},
}
: {
style: {
background: "red",
},
tabindex: "2",
onClick: () => {},
};
};
useEffect(
() => {
setToggle(false);
},
() => []
);
return () => {
return hello
;
};
};
testRender( , root);
expect(root.innerHTML).toBe(
'hello
'
);
await rendered();
expect(root.innerHTML).toBe(
'hello
'
);
});
================================================
FILE: packages/unis-dom/src/browser/__test__/effect.test.tsx
================================================
/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, expect, it } from "vitest";
import { useEffect } from "@unis/core";
import { useState } from "@unis/core";
import { rendered, testRender } from "./util";
let root: Element;
beforeEach(() => {
root = document.createElement("div");
document.body.append(root);
});
afterEach(() => {
root.innerHTML = "";
});
it("effect", async () => {
const Bpp = () => {
useEffect(
() => {
return () => {};
},
() => []
);
return () => "bpp";
};
const App = () => {
let [visible, setVisible] = useState(true);
useEffect(
() => {
setVisible(false);
},
() => []
);
return () => (visible ? : null);
};
testRender( , root);
expect(root.innerHTML).toBe("bpp");
await rendered();
expect(root.innerHTML).toBe("");
});
================================================
FILE: packages/unis-dom/src/browser/__test__/hydrate.test.tsx
================================================
/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, expect, it } from "vitest";
import { rendered, testRender } from "./util";
import { use, useState } from "@unis/core";
let root: Element;
beforeEach(() => {
root = document.createElement("div");
document.body.append(root);
});
afterEach(() => {
root.innerHTML = "";
});
it("hydrate", async () => {
root.innerHTML = "Apphello
";
let setMsgOutter: any;
const App = () => {
let [msg, setMsg] = useState("hello");
use(() => {
setMsgOutter = setMsg;
});
return () => (
App{msg}
);
};
testRender( , root, true);
expect(root.innerHTML).toBe("Apphello
");
setMsgOutter("world");
await rendered();
expect(root.innerHTML).toBe("Appworld
");
});
================================================
FILE: packages/unis-dom/src/browser/__test__/memo.test.tsx
================================================
/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, expect, it } from "vitest";
import { useEffect } from "@unis/core";
import { useState } from "@unis/core";
import { h2, memo } from "@unis/core";
import { rendered, testRender } from "./util";
let root: Element;
beforeEach(() => {
root = document.createElement("div");
document.body.append(root);
});
afterEach(() => {
root.innerHTML = "";
});
it("memo", async () => {
const Bpp = memo(() => {
let renderCount = 0;
return () => {
return {renderCount++}
;
};
});
const App = () => {
let [msg, setMsg] = useState("hello");
useEffect(
() => {
setMsg("hello world");
},
() => []
);
return () => (
{msg}
{h2(Bpp, {}, "key")}
);
};
testRender( , root);
expect(root.innerHTML).toBe("");
await rendered();
expect(root.innerHTML).toBe("");
});
================================================
FILE: packages/unis-dom/src/browser/__test__/portal.test.tsx
================================================
/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, expect, it } from "vitest";
import { useEffect } from "@unis/core";
import { useState } from "@unis/core";
import { createPortal, Fragment } from "@unis/core";
import { rendered, testRender } from "./util";
let root: Element;
let dialog: Element;
beforeEach(() => {
root = document.createElement("div");
dialog = document.createElement("div");
document.body.append(root, dialog);
});
afterEach(() => {
root.innerHTML = "";
dialog.innerHTML = "";
});
it("portal", async () => {
const App = () => {
let [visible, setVisible] = useState(true);
useEffect(
() => {
setVisible(false);
},
() => []
);
return () => (
hello
{visible && createPortal(hello dialog , dialog)}
);
};
testRender( , root);
expect(document.body.innerHTML).toBe(
"hello dialog
"
);
await rendered();
expect(document.body.innerHTML).toBe(
"
"
);
});
================================================
FILE: packages/unis-dom/src/browser/__test__/reconcile.test.tsx
================================================
/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, expect, it } from "vitest";
import { useEffect } from "@unis/core";
import { useState } from "@unis/core";
import { rendered, testRender } from "./util";
let root: Element;
beforeEach(() => {
root = document.createElement("div");
document.body.append(root);
});
afterEach(() => {
root.innerHTML = "";
});
it("diff with key", async () => {
const App = () => {
let [toggle, setToggle] = useState(false);
useEffect(
() => {
setToggle(true);
},
() => []
);
return () =>
!toggle ? (
) : (
);
};
testRender( , root);
expect(root.innerHTML).toBe(
''
);
await rendered();
expect(root.innerHTML).toBe(
''
);
});
================================================
FILE: packages/unis-dom/src/browser/__test__/util.ts
================================================
import { readyForWork, createRoot, createTokTik } from "@unis/core";
import { createOperator } from "../operator";
const toktik = createTokTik({
nextTick: (cb: VoidFunction) =>
Promise.resolve()
.catch((err) => console.error(err))
.then(() => cb()),
now: () => 0,
});
const operator = createOperator();
export const testRender = (
element: any,
container: Element,
hydrate = false
) => {
const rootFiber = createRoot(element, container);
rootFiber.runtime = {
toktik,
operator,
};
readyForWork(rootFiber, hydrate);
};
export const rendered = () =>
new Promise((resolve) => {
setTimeout(resolve, 0);
});
================================================
FILE: packages/unis-dom/src/browser/__test__/utils.test.ts
================================================
/**
* @vitest-environment jsdom
*/
import { expect, it } from "vitest";
import { classes, svgKey, styleStr } from "@unis/core";
it("classes", () => {
expect(classes(["a", "b", 1, ["c", { d: true }]])).toBe("a b 1 c d");
expect(classes({ a: true, b: undefined, c: null })).toBe("a");
expect(classes({ a: false, b: true, c: null })).toBe("b");
});
it("realSVGAttr", () => {
expect(svgKey("glyphOrientationVertical")).toBe("glyph-orientation-vertical");
});
it("style2String", () => {
expect(styleStr({ background: "yellow", fontSize: "14px" })).toBe(
"background: yellow; font-size: 14px;"
);
});
================================================
FILE: packages/unis-dom/src/browser/const.ts
================================================
export const UNIS_ROOT = Symbol("unis_root");
================================================
FILE: packages/unis-dom/src/browser/index.ts
================================================
export { UNIS_ROOT } from "./const";
export { render } from "./render";
================================================
FILE: packages/unis-dom/src/browser/operator.ts
================================================
import { Fiber, findEls, isPortal, isText, Operator } from "@unis/core";
import { getEventName, isEvent, isNullish } from "@unis/core";
import { UNIS_ROOT } from "./const";
type FiberDomEl = Element | DocumentFragment | SVGAElement | Text | ParentNode;
interface FiberDom extends Fiber {
el?: FiberDomEl;
}
export const createOperator = (): Operator => {
const createElement = (fiber: FiberDom) => {
const { tag: type, isSvg } = fiber;
return isText(fiber)
? document.createTextNode(fiber.props.nodeValue + "")
: isSvg
? document.createElementNS("http://www.w3.org/2000/svg", type as string)
: document.createElement(type as string);
};
const nextElement = (el: FiberDomEl | null) => {
while (el) {
if (el.firstChild) return el.firstChild;
if (el.nextSibling) return el.nextSibling;
while ((el = el.parentNode)) {
if ((el as any)[UNIS_ROOT]) return null;
if (el.nextSibling) return el.nextSibling;
}
}
return null;
};
const matchElement = (fiber: FiberDom, el: Element | Text) =>
el.nodeType === Node.TEXT_NODE
? isText(fiber)
: (el as Element).tagName.toLocaleLowerCase() === fiber.tag;
const insertBefore = (
containerFiber: FiberDom,
insertElement: FiberDomEl,
targetElement: FiberDomEl | null
) => {
(
(isPortal(containerFiber)
? containerFiber.to
: containerFiber.el)! as FiberDomEl
).insertBefore(insertElement, targetElement);
};
const nextSibling = (fiber: FiberDom) => fiber.el!.nextSibling;
const firstChild = (fiber: FiberDom) => fiber.el!.firstChild;
const remove = (fiber: FiberDom) => {
const [first, ...rest] = findEls(fiber) as FiberDomEl[];
const parentNode = first?.parentNode;
if (parentNode) {
for (const el of [first, ...rest]) {
parentNode.removeChild(el);
}
}
};
const updateTextProperties = (fiber: FiberDom) => {
(fiber.el! as Text).nodeValue = fiber.props.nodeValue + "";
};
const setAttr = (
el: SVGAElement | HTMLElement,
isSvg: boolean,
key: string,
value: string
) =>
isSvg
? (el as SVGAElement).setAttributeNS(null, key, value)
: (el as HTMLElement).setAttribute(key, value);
const removeAttr = (
el: SVGAElement | HTMLElement,
isSvg: boolean,
key: string
) =>
isSvg
? (el as SVGAElement).removeAttributeNS(null, key)
: (el as HTMLElement).removeAttribute(key);
const updateElementProperties = (fiber: FiberDom) => {
let { el, isSvg, attrDiff } = fiber;
for (const [key, newValue, oldValue] of attrDiff || []) {
const newExist = !isNullish(newValue);
const oldExist = !isNullish(oldValue);
if (key === "ref") {
oldExist && (oldValue.current = undefined);
newExist && (newValue.current = el);
} else if (isEvent(key)) {
const [eventName, capture] = getEventName(key);
oldExist && el!.removeEventListener(eventName, oldValue);
newExist && el!.addEventListener(eventName, newValue, capture);
} else {
newExist
? setAttr(el as SVGAElement | HTMLElement, isSvg!, key, newValue)
: removeAttr(el as SVGAElement | HTMLElement, isSvg!, key);
}
}
};
return {
createElement,
nextElement,
matchElement,
insertBefore,
nextSibling,
firstChild,
remove,
updateTextProperties,
updateElementProperties,
};
};
================================================
FILE: packages/unis-dom/src/browser/render.ts
================================================
import { readyForWork, createRoot, createTokTik } from "@unis/core";
import { createOperator } from "./operator";
import { UNIS_ROOT } from "./const";
import { nextTick, now } from "./toktik";
const operator = createOperator();
const toktik = createTokTik({
now,
nextTick,
interval: (window as any).UNIS_INTERVAL,
});
export const render = (element: any, container: Element, hydrate = false) => {
(container as any)[UNIS_ROOT] = true;
const rootFiber = createRoot(element, container);
rootFiber.runtime = {
toktik,
operator,
};
readyForWork(rootFiber, hydrate);
};
================================================
FILE: packages/unis-dom/src/browser/toktik.ts
================================================
export const nextTick = (cb: VoidFunction, pending = false) => {
if (pending) {
queueMicrotask(cb);
} else if (window.MessageChannel) {
const { port1, port2 } = new window.MessageChannel();
port1.postMessage("");
port2.onmessage = () => cb();
} else {
setTimeout(() => cb());
}
};
export const now = () => performance.now();
================================================
FILE: packages/unis-dom/src/server/__test__/server.test.tsx
================================================
import { expect, it } from "vitest";
import { renderToString } from "..";
it("render to string", () => {
const App = () => {
return () => (
<>
hello
world
>
);
};
const result = renderToString( );
expect(result).toBe(
'
hello world '
);
});
================================================
FILE: packages/unis-dom/src/server/index.ts
================================================
import { createRoot, createTokTik, readyForWork } from "@unis/core";
import { createOperator, ElementNode } from "./operator";
const operator = createOperator();
const toktik = createTokTik({
nextTick: (cb: VoidFunction) =>
Promise.resolve()
.catch((err) => console.error(err))
.then(() => cb()),
now: () => 0,
});
export const renderToString = (element: any) => {
const rootNode = new ElementNode("");
const rootFiber = createRoot(element, rootNode);
rootFiber.runtime = {
toktik,
operator,
};
readyForWork(rootFiber);
return rootNode.renderToString();
};
================================================
FILE: packages/unis-dom/src/server/operator.ts
================================================
import { isEvent, isNullish, Operator, Fiber, isText } from "@unis/core";
export class ElementNode {
children: ServerNode[] = [];
properties: Record = {};
constructor(public tagName: string) {}
insertBefore(
node: ElementNode | TextNode,
child: ElementNode | TextNode | null
) {
this.children.push(node);
}
append(...nodes: ServerNode[]) {
this.children.push(...nodes);
}
renderToString(): string {
const children = this.children
.map((child) => child.renderToString())
.join("");
const propertiesStr = Object.keys(this.properties)
.map((key) => `${key}="${this.properties[key]}"`)
.join(" ");
const gap = propertiesStr ? " " : "";
return this.tagName
? `<${this.tagName}${gap}${propertiesStr}>${children}${this.tagName}>`
: children;
}
}
export class TextNode {
constructor(public nodeValue: string) {}
renderToString() {
return this.nodeValue;
}
}
export type ServerNode = ElementNode | TextNode;
interface FiberDomServer extends Fiber {
el?: ServerNode;
}
export const createOperator = (): Operator => {
const createElement = (fiber: FiberDomServer) => {
const { tag: type } = fiber;
return isText(fiber)
? new TextNode(fiber.props.nodeValue + "")
: new ElementNode(type as string);
};
const insertBefore = (
containerFiber: FiberDomServer,
insertElement: ServerNode,
targetElement: ServerNode
) => {
(containerFiber.el as ElementNode).insertBefore(
insertElement,
targetElement
);
};
const firstChild = (fiber: FiberDomServer) =>
(fiber.el as ElementNode).children[0] ?? null;
const updateTextProperties = (fiber: FiberDomServer) => {
(fiber.el as TextNode).nodeValue = fiber.props.nodeValue + "";
};
const updateElementProperties = (fiber: FiberDomServer) => {
let { el, attrDiff } = fiber;
for (const [key, newValue, oldValue] of attrDiff || []) {
const newExist = !isNullish(newValue);
const oldExist = !isNullish(oldValue);
if (key === "ref") {
oldExist && (oldValue.current = undefined);
newExist && (newValue.current = el);
} else if (isEvent(key)) {
// nothing...
} else {
newExist
? ((el as ElementNode).properties[key] = newValue)
: delete (el as ElementNode).properties[key];
}
}
};
return {
nextElement() {},
matchElement: () => false,
remove() {},
nextSibling() {},
createElement,
insertBefore,
firstChild,
updateTextProperties,
updateElementProperties,
};
};
================================================
FILE: packages/unis-dom/tsconfig.build.json
================================================
{
"extends": "./tsconfig.json",
"exclude": ["**/*.test.*", "**/__test__"]
}
================================================
FILE: packages/unis-dom/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "build"
},
"include": ["src"]
}
================================================
FILE: packages/unis-dom/vitest.config.ts
================================================
import { defineConfig } from "vitest/config";
import { unisPreset } from "@unis/vite-preset";
export default defineConfig({
plugins: [unisPreset()],
test: {
include: ["**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
coverage: {
reporter: ["text", "json", "html"],
},
},
});
================================================
FILE: packages/unis-example/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
build
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
.DS_Store
================================================
FILE: packages/unis-example/index.html
================================================
Unis
================================================
FILE: packages/unis-example/other.d.ts
================================================
declare module "*.css";
================================================
FILE: packages/unis-example/package.json
================================================
{
"name": "@unis/unis-example",
"version": "0.0.0",
"description": "",
"main": "index.js",
"private": "true",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/anuoua/unis.git"
},
"author": "anuoua",
"license": "MIT",
"bugs": {
"url": "https://github.com/anuoua/unis/issues"
},
"homepage": "https://github.com/anuoua/unis#readme",
"dependencies": {
"@unis/core": "workspace:^",
"@unis/dom": "workspace:^",
"@unis/router": "workspace:^",
"@unis/transition": "workspace:^"
},
"devDependencies": {
"@types/lodash": "^4.14.182",
"@unis/vite-preset": "workspace:^",
"autoprefixer": "^10.4.0",
"postcss": "^8.3.11",
"rollup-plugin-reassign": "^1.0.2",
"tailwindcss": "^3.3.1",
"vite": "^4.1.2"
}
}
================================================
FILE: packages/unis-example/postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
================================================
FILE: packages/unis-example/src/Dialog.tsx
================================================
import { createPortal, useProps, useRef } from "@unis/core";
import { Item } from "./TodoItem";
interface DialogProps {
onClose: () => void;
onConfirm: (item: Item) => void;
item: Item | null;
}
export function Dialog(props: DialogProps) {
let { item, onClose, onConfirm } = useProps(props);
const portalRef = useRef();
const handleClose = (e: MouseEvent) => {
if ((e.target as HTMLElement) === portalRef.current) {
onClose();
}
};
const handleConfirm = () => {
onConfirm(item!);
};
return () =>
createPortal(
{" "}
Delete {item?.name} ?
Confirm
,
document.querySelector("#dialog")!
);
}
================================================
FILE: packages/unis-example/src/Todo.tsx
================================================
import { useState } from "@unis/core";
import { CSSTransition, TransitionGroup } from "@unis/transition";
import { Dialog } from "./Dialog";
import { Item, TodoItem } from "./TodoItem";
let count = 0;
let todos: any[] = [];
for (; count < 0; count++) {
todos.push({
id: count,
name: "",
editing: false,
canceled: false,
});
}
export function ToDo() {
let [todoList, setTodoList] = useState(todos);
let [dialogVisible, setDialogVisible] = useState(false);
let [titleVisible, setTitleVisible] = useState(true);
let [currentItem, setCurrentItem] = useState- (null);
const handleToggleTitle = () => {
setTitleVisible(!titleVisible);
setTimeout(() => {
setTitleVisible(true);
}, 200);
};
const handleAdd = (e: any) => {
if (e.key !== "Enter") return;
setTodoList([
...todoList,
{
id: ++count,
name: e.target.value,
editing: false,
canceled: false,
},
]);
e.target.value = "";
};
const handleClose = () => {
setDialogVisible(false);
};
const handleConfirm = (item: Item) => {
setDialogVisible(false);
setTodoList(todoList.filter((i) => i !== item));
};
const handleDelete = (item: Item) => {
setCurrentItem(item);
setTodoList(todoList.filter((i) => i !== item));
// setDialogVisible(true);
};
return () => (
);
}
================================================
FILE: packages/unis-example/src/TodoItem/index.module.css
================================================
.deleteIcon {
color: white;
}
================================================
FILE: packages/unis-example/src/TodoItem/index.tsx
================================================
import { use, memo, useEffect, useProps, useRef } from "@unis/core";
import { Update } from "../hooks/update";
import s from "./index.module.css";
export interface Item {
id: number;
name: string;
editing: boolean;
canceled: boolean;
}
interface TodoItemProps {
item: Item;
onDelete: (item: Item) => any;
}
export const TodoItem = memo((props: TodoItemProps) => {
let { item, onDelete } = useProps(props);
let [render] = use(Update());
let { editing, canceled, name } = use(() => item);
const inputRef = useRef();
const handleEditing = () => {
if (canceled) return;
item.editing = true;
render();
};
const handleClick = () => {
onDelete(item);
};
const handleCancel = () => {
console.log(inputRef.current);
item.canceled = !item.canceled;
render();
};
const handleKeyDown = (e: any) => {
if (e.key !== "Enter") return;
item.name = e.target.value;
item.editing = false;
render();
};
const handleBlur = () => {
item.editing = false;
render();
};
useEffect(
() => {
item.name = item.name + "x";
render();
},
() => [item.canceled]
);
return () => {
// console.log("TodoItem render");
return (
{editing ? (
) : canceled ? (
{name}
) : (
{name}
)}
{canceled ? (
) : (
)}
);
};
});
================================================
FILE: packages/unis-example/src/Welcome/index.module.css
================================================
.msg {
font-size: 50px;
font-weight: bold;
background: -webkit-linear-gradient(0deg,#e339bc 25%,#4f5b94);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
transition: all .3s ease;
}
.msg:hover {
cursor: pointer;
transform: scale(1.2);
background: -webkit-linear-gradient(0deg,#4f5b94 25%,#e339bc);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
================================================
FILE: packages/unis-example/src/Welcome/index.tsx
================================================
import { Link } from "@unis/router";
import s from "./index.module.css";
export const Welcome = () => {
return () => (
Unis Todo
);
};
================================================
FILE: packages/unis-example/src/global.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body,
#root {
height: 100%;
}
.scale-appear {
transform: scale(0) translateZ(0);
}
.scale-appear-active {
transform: scale(1) translateZ(0);
transition: all 0.4s ease;
}
.scale-appear-done {
transform: scale(1) translateZ(0);
}
.scale-enter {
transform: scale(0) translateZ(0);
}
.scale-enter-active {
transform: scale(1) translateZ(0);
transition: all 0.4s ease;
}
.scale-enter-done {
transform: scale(1) translateZ(0);
}
.scale-exit {
transform: scale(1) translateZ(0);
}
.scale-exit-active {
transform: scale(0) translateZ(0);
transition: all 0.4s ease;
}
.scale-exit-done {
transform: scale(0) translateZ(0);
}
.fade-enter {
opacity: 0;
}
.fade-enter-active {
opacity: 1;
transition: all 0.4s ease;
}
.fade-enter-done {
opacity: 1;
}
.fade-exit {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
transition: all 0.4s ease;
}
.fade-exit-done {
opacity: 0;
}
================================================
FILE: packages/unis-example/src/hooks/update.ts
================================================
import { useState } from "@unis/core";
export const Update = () => {
let [count, setCount] = useState(1);
const update = () => setCount(++count);
return () => [update];
};
================================================
FILE: packages/unis-example/src/index.module.css
================================================
.background_img {
background: url(./bg.jpeg);
z-index: -1;
background-size: cover;
background-position: center;
}
================================================
FILE: packages/unis-example/src/index.tsx
================================================
import {
Fragment,
// useProps,
// useState,
// useEffect
} from "@unis/core";
import { render } from "@unis/dom";
import { ToDo } from "./Todo";
import "./global.css";
import s from "./index.module.css";
import { BrowserRouter, Redirect, Outlet, Route, Routes } from "@unis/router";
import { Welcome } from "./Welcome";
// const Bpp = (props: { time: number; msg: string }) => {
// let { time, msg } = useProps(props);
// let [visible, setVisible] = useState(false);
// useEffect(
// () => {
// setVisible(true);
// setTimeout(() => {
// console.log("timeout");
// setVisible(false);
// }, 0);
// },
// () => []
// );
// setTimeout(() => {
// setVisible(true);
// }, time);
// return () => (visible ? {msg}
: false);
// };
const App = () => {
return () => (
<>
{/* */}
{/* */}
>
{/* */}
);
};
render(
}>
} />
} />
} />
,
document.querySelector("#root")!
);
================================================
FILE: packages/unis-example/tailwind.config.js
================================================
module.exports = {
content: ["./public/**/*.html", "./src/**/*.{js,jsx,ts,tsx,vue}"],
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
================================================
FILE: packages/unis-example/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"include": ["src", "other.d.ts"]
}
================================================
FILE: packages/unis-example/vite.config.js
================================================
import { defineConfig } from "vite";
import { unisPreset } from "@unis/vite-preset";
export default defineConfig({
plugins: [unisPreset()],
});
================================================
FILE: packages/unis-router/.gitignore
================================================
build/
coverage/
dist/
================================================
FILE: packages/unis-router/README.md
================================================
# Unis Router
Router for unis, inspire by [React Router V6](https://github.com/remix-run/react-router).
## Install
```shell
npm i @unis/router
```
## Usage
Unis router's api is partial same as React Router V6.
example
```javascript
import { BrowserRouter, Routes, Route, Outlet } from '@unis/router'
const Dashboard = () => {
return () => (
dashboard
// hello
)
}
const App = () => {
return () => (
}>
hello
} />
)
}
render(
, document.querySelector('#app'))
```
================================================
FILE: packages/unis-router/package.json
================================================
{
"name": "@unis/router",
"version": "0.1.0",
"description": "Unis router component",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"typings": "dist/index.d.ts",
"scripts": {
"build": "rimraf build && rimraf dist && tsc -p tsconfig.json && rollup --config",
"build:dev": "cross-env NODE_ENV=development pnpm build",
"test": "vitest run --coverage",
"test:watch": "vitest -w"
},
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs"
}
},
"keywords": [
"unis",
"router"
],
"files": [
"dist"
],
"author": "anuoua",
"peerDependencies": {
"@unis/core": "workspace:^"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/anuoua/unis/issues"
},
"homepage": "https://github.com/anuoua/unis/tree/main/packages/unis-router",
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.2",
"@types/node": "^18.15.11",
"@unis/core": "workspace:^",
"@unis/vite-preset": "workspace:^",
"@vitest/coverage-c8": "^0.29.8",
"cross-env": "^7.0.3",
"esbuild": "^0.17.15",
"rimraf": "^4.4.1",
"rollup": "^3.20.2",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0",
"rollup-plugin-reassign": "^1.0.3",
"typescript": "^5.0.3",
"vitest": "^0.29.8"
},
"dependencies": {
"history": "^5.3.0"
}
}
================================================
FILE: packages/unis-router/rollup.config.mjs
================================================
import esbuild from "rollup-plugin-esbuild";
import dts from "rollup-plugin-dts";
import { defineConfig } from "rollup";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import { reassign } from "rollup-plugin-reassign";
import { unisFns } from "@unis/core";
const configGen = (format) =>
defineConfig({
input: "src/index.ts",
external: [/^@unis/, "history"],
output: [
{
dir: "dist",
entryFileNames: `index.${format === "esm" ? "mjs" : "js"}`,
format,
sourcemap: true,
},
],
plugins: [
nodeResolve(),
esbuild({
sourceMap: true,
target: "esnext",
jsx: "automatic",
jsxImportSource: "@unis/core",
}),
reassign({
include: ["**/*.(t|j)s?(x)"],
targetFns: {
"@unis/core": unisFns,
},
}),
],
});
const dtsRollup = () =>
defineConfig({
input: "build/index.d.ts",
output: [{ file: "dist/index.d.ts", format: "es" }],
plugins: [dts()],
});
const config = [configGen("cjs"), configGen("esm"), dtsRollup()];
export default config;
================================================
FILE: packages/unis-router/src/components/BrowserRouter.tsx
================================================
import { useProps, useLayoutEffect, useState, useMemo } from "@unis/core";
import { createBrowserHistory, BrowserHistory } from "history";
import { LocationContext, RouterContext } from "../context";
export interface BrowserRouterProps {
children?: JSX.Element | JSX.Element[];
history?: BrowserHistory;
basename?: string;
}
export const BrowserRouter = (p: BrowserRouterProps) => {
let { children, history, basename } = useProps(p);
const historyInstance = history ?? createBrowserHistory();
let [location, setLocation] = useState(historyInstance.location);
const navigationContextValue = {
basename: basename ?? "",
history: historyInstance,
};
let locationContextValue = useMemo(
() => ({ location }),
() => [location]
);
useLayoutEffect(
() => {
const unlisten = historyInstance.listen(({ location }) => {
setLocation(location);
});
return () => unlisten();
},
() => []
);
return () => (
{children}
);
};
================================================
FILE: packages/unis-router/src/components/Link.tsx
================================================
import {
AnchorHTMLAttributes,
ElementAttrs,
use,
useContext,
useProps,
} from "@unis/core";
import { RouterContext } from "../context";
import { uTargetPath } from "../hooks/uTargetPath";
export type LinkProps = Partial<
Omit, "href"> & {
to: string;
}
>;
export const Link = (p: LinkProps) => {
let { to, children, onClick, ...rest } = useProps(p);
let { history } = useContext(RouterContext);
let targetPath = use(uTargetPath(() => ({ to })));
const handleJump = (e: MouseEvent) => {
const target = (e.target as HTMLAnchorElement).target;
if (
e.button === 0 &&
(!target || target === "_self") &&
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
) {
if (history.location.pathname !== targetPath) {
history.push(targetPath);
} else {
history.replace(targetPath);
}
e.preventDefault();
}
onClick?.(e);
};
return () => (
{children}
);
};
================================================
FILE: packages/unis-router/src/components/NavLink.tsx
================================================
import { HTMLAttributes, use, useContext, useProps } from "@unis/core";
import { RouteContext } from "../context";
import { uTargetPath } from "../hooks/uTargetPath";
import { Link, LinkProps } from "./Link";
export type LinkStyle = (isActive: boolean) => HTMLAttributes["style"];
export type LinkClassName = (isActive: boolean) => HTMLAttributes["className"];
export type NavLinkProps = Omit & {
style?: LinkStyle;
className?: LinkClassName;
};
export const NavLink = (p: NavLinkProps) => {
let { style, className, to, ...rest } = useProps(p);
let { matches } = useContext(RouteContext);
let targetPath = use(uTargetPath(() => ({ to })));
let isActive = use(() => !!matches.find((i) => i.pathname === targetPath));
return () => (
);
};
================================================
FILE: packages/unis-router/src/components/Outlet.tsx
================================================
import { use, useContext } from "@unis/core";
import { RouteContext } from "../context";
export const Outlet = () => {
let { matches, route } = useContext(RouteContext);
let nextRoute = use(() => matches[matches.findIndex((i) => i === route) + 1]);
let { element, ...rest } = use(() => route ?? {});
return () =>
route ? (
{element}
) : null;
};
================================================
FILE: packages/unis-router/src/components/Redirect.tsx
================================================
import { useContext, useEffect, useProps } from "@unis/core";
import { RouteContext, RouterContext } from "../context";
import { resolvePath } from "../utils";
export interface RedirectProps {
to?: string;
replace?: boolean;
}
export const Redirect = (p: RedirectProps) => {
let { to, replace = true } = useProps(p);
let { history } = useContext(RouterContext);
let { route } = useContext(RouteContext);
useEffect(
() => {
if (route) {
const pathname = resolvePath(route.pathname!, to ?? "");
replace ? history.replace(pathname) : history.push(pathname);
}
},
() => []
);
return () => null;
};
================================================
FILE: packages/unis-router/src/components/Route.tsx
================================================
import { RouteData } from "../types";
export type RouteProps = Omit & {
children?: JSX.Element | JSX.Element[];
};
export const Route = (p: RouteProps) => null;
================================================
FILE: packages/unis-router/src/components/Routes.tsx
================================================
import { cloneElement, FiberNode, use, useProps } from "@unis/core";
import { uRouter } from "../hooks/uRouter";
import { Route } from "./Route";
import { RouteData } from "../types";
export type RoutesProps = Omit & {
children?: JSX.Element | JSX.Element[];
};
export const Routes = (p: RoutesProps) => {
let { children, path, element: incomeElement } = useProps(p);
let realChildren = use(() => flatChildren(children));
let routes = use(() => realChildren.map((node) => pick(node)));
let element = use(
uRouter(() => [
{
path,
element: incomeElement,
children: routes,
} as RouteData,
])
);
function pick(node: FiberNode): RouteData {
return {
...node.props,
path: node.props.path,
element: cloneElement(node.props.element),
children: flatChildren(node.props.children).map(pick),
};
}
function flatChildren(children: JSX.Element | JSX.Element[]) {
return ([] as FiberNode[])
.concat(children as FiberNode[])
.filter((child) => child?.tag === Route);
}
return () => element;
};
================================================
FILE: packages/unis-router/src/context.tsx
================================================
import { createContext } from "@unis/core";
import { BrowserHistory, Location } from "history";
import { MatchRoute } from "./types";
export interface RouteContextValue {
route: MatchRoute;
matches: MatchRoute[];
}
export const RouteContext = createContext({
route: undefined!,
matches: [],
});
export interface RouterContextValue {
history: BrowserHistory;
basename: string;
}
export const RouterContext = createContext(undefined!);
export interface LocationContextValue {
location: Location;
}
export const LocationContext = createContext(undefined!);
================================================
FILE: packages/unis-router/src/hooks/uHistory.ts
================================================
import { useContext } from "@unis/core";
import { RouterContext } from "../context";
export const uHistory = () => {
let { history } = useContext(RouterContext);
return () => history!;
};
================================================
FILE: packages/unis-router/src/hooks/uLocation.ts
================================================
import { use } from "@unis/core";
import { uHistory } from "./uHistory";
export const uLocation = () => {
let { location } = use(uHistory());
return () => location;
};
================================================
FILE: packages/unis-router/src/hooks/uParams.ts
================================================
import { useContext } from "@unis/core";
import { RouteContext } from "../context";
export const uParams = >() => {
let { route } = useContext(RouteContext);
return () => route?.params as unknown as T;
};
================================================
FILE: packages/unis-router/src/hooks/uRouter.tsx
================================================
import { use, useContext } from "@unis/core";
import { RouteContext, RouterContext } from "../context";
import { Outlet } from "../components/Outlet";
import { RouteData } from "../types";
import { matchRoutes } from "../utils";
export const uRouter = (configFn: () => RouteData[]) => {
let routerData = use(configFn);
let { history, basename } = useContext(RouterContext);
let location = use(() => history?.location!);
let wrapedRouterData = use(() => [
{
path: basename,
element: ,
children: routerData,
} as RouteData,
]);
let matches = use(() => matchRoutes(location.pathname, wrapedRouterData));
return () => (
);
};
================================================
FILE: packages/unis-router/src/hooks/uTargetPath.tsx
================================================
import { use, useContext } from "@unis/core";
import { RouteContext } from "../context";
import { resolvePath } from "../utils";
export interface Options {
to?: string;
}
export const uTargetPath = (opts: () => Options) => {
let { to } = use(opts);
let { route } = useContext(RouteContext);
let targetPath = use(() => resolvePath(route.pathname ?? "", to ?? ""));
return () => targetPath;
};
================================================
FILE: packages/unis-router/src/index.ts
================================================
export * from "./components/BrowserRouter";
export * from "./context";
export * from "./components/Routes";
export * from "./components/Route";
export * from "./components/Outlet";
export * from "./components/Link";
export * from "./components/NavLink";
export * from "./components/Redirect";
export * from "./hooks/uHistory";
export * from "./hooks/uRouter";
export * from "./hooks/uLocation";
export * from "./hooks/uHistory";
export * from "./hooks/uParams";
================================================
FILE: packages/unis-router/src/types.ts
================================================
export interface RouteData {
path?: string;
element?: JSX.Element;
children?: RouteData[];
}
export interface MatchRoute extends RouteData {
pathname?: string;
params?: Record;
}
================================================
FILE: packages/unis-router/src/utils.test.tsx
================================================
import { expect, it } from "vitest";
import { RouteData } from "./types";
import { matchRoutes, resolvePath } from "./utils";
const getRoutes = (): RouteData[] => [
{
path: "/",
children: [
{
path: "home/:from/",
children: [
{ path: "*" },
{
path: "post/:id",
},
{
path: "post/1",
children: [
{
children: [{ path: "xx" }],
},
],
},
],
},
],
},
];
it("root match", () => {
const matchedRoutes = matchRoutes("/home/www", getRoutes());
expect(matchedRoutes).toMatchObject([
{ path: "/", params: {}, pathname: "/" },
{
path: "home/:from/",
params: { from: "www" },
pathname: "/home/www",
},
]);
});
it("match test1", () => {
const matchedRoutes = matchRoutes("/home/www/post", getRoutes());
expect(matchedRoutes).toMatchObject([
{ path: "/", params: {}, pathname: "/" },
{
path: "home/:from/",
params: { from: "www" },
pathname: "/home/www",
},
{ path: "*", params: { from: "www" }, pathname: "/home/www/post" },
]);
});
it("match test2", () => {
const matchedRoutes = matchRoutes("/home/www/post/1", getRoutes());
expect(matchedRoutes).toMatchObject([
{ path: "/", params: {}, pathname: "/" },
{
path: "home/:from/",
params: { from: "www" },
pathname: "/home/www",
},
{
path: "post/1",
params: { from: "www" },
pathname: "/home/www/post/1",
},
{ params: { from: "www" }, pathname: "/home/www/post/1" },
]);
});
it("match test3", () => {
const matchedRoutes = matchRoutes("/home/www/post/2", getRoutes());
expect(matchedRoutes).toMatchObject([
{ path: "/", params: {}, pathname: "/" },
{
path: "home/:from/",
params: { from: "www" },
pathname: "/home/www",
},
{
path: "post/:id",
params: { from: "www", id: "2" },
pathname: "/home/www/post/2",
},
]);
});
it("match test4", () => {
const matchedRoutes = matchRoutes("/home/www/post/1/x", getRoutes());
expect(matchedRoutes).toMatchObject([
{ path: "/", params: {}, pathname: "/" },
{
path: "home/:from/",
params: { from: "www" },
pathname: "/home/www",
},
{
path: "*",
params: { from: "www" },
pathname: "/home/www/post/1/x",
},
]);
});
it("match test5", () => {
const matchedRoutes = matchRoutes("/home/www/post/1/xx", getRoutes());
expect(matchedRoutes).toMatchObject([
{ path: "/", params: {}, pathname: "/" },
{
path: "home/:from/",
params: { from: "www" },
pathname: "/home/www",
},
{
path: "post/1",
params: { from: "www" },
pathname: "/home/www/post/1",
},
{
params: { from: "www" },
pathname: "/home/www/post/1",
},
{
path: "xx",
params: { from: "www" },
pathname: "/home/www/post/1/xx",
},
]);
});
it("match test6", () => {
const matchedRoutes = matchRoutes("/app/home/www/post/1/x", getRoutes(), [
{ path: "/app" },
]);
expect(matchedRoutes).toMatchObject([
{ path: "/app", params: {}, pathname: "/app" },
{ path: "/", params: {}, pathname: "/app" },
{
path: "home/:from/",
params: { from: "www" },
pathname: "/app/home/www",
},
{
path: "*",
params: { from: "www" },
pathname: "/app/home/www/post/1/x",
},
]);
});
it("resolvePath", () => {
const path = resolvePath("/home/c", "../a");
expect(path).toBe("/a");
const path2 = resolvePath("/home/c", "./a");
expect(path2).toBe("/home/a");
const path3 = resolvePath("/home/c", "/a");
expect(path3).toBe("/a");
});
================================================
FILE: packages/unis-router/src/utils.ts
================================================
import { MatchRoute, RouteData } from "./types";
const SLASH = "/";
const DOT = ".";
const trimSlash = (str: string) => {
const reg = new RegExp(`^[${SLASH}${DOT}]*`);
const reg2 = new RegExp(`[${SLASH}${DOT}]*$`);
return str.replace(reg, "").replace(reg2, "");
};
const split = (path: string) =>
trimSlash(path) ? trimSlash(path).split(SLASH) : [];
const analysePath = (locationPathname: string, routePath: string) => {
const locationChunks = split(locationPathname);
const routeChunks = split(routePath);
const params: Record = {};
for (let i = 0; i < routeChunks.length; i++) {
const routeChunk = routeChunks[i];
if (routeChunk.startsWith(":")) {
params[routeChunk.slice(1)] = locationChunks[i];
}
}
return {
params,
pathname:
SLASH +
(locationChunks
.slice(
0,
routeChunks?.at(-1) === "*" ? undefined : routeChunks?.length ?? 0
)
.join(SLASH) ?? ""),
};
};
const testPath = (
locationPathname: string,
routePath: string,
final = false
) => {
const locationChunks = split(locationPathname);
const routeChunks = split(routePath);
for (let i = 0; i < routeChunks.length; i++) {
const routeChunk = routeChunks[i]!;
const locationChunk = locationChunks[i];
if (
!locationChunk ||
(!routeChunk.startsWith(":") &&
routeChunk !== "*" &&
routeChunk !== locationChunk)
) {
return false;
}
}
if (final && locationChunks.length > routeChunks.length) {
return routeChunks.at(-1) === "*";
}
return true;
};
const getScore = (locationPathname: string, routeChain: RouteData[]) => {
const routePath = resolveRoutesPath(routeChain);
const locationChunks = split(locationPathname);
const routeChunks = split(routePath);
let score = 0;
routeChunks.forEach((chunk, index) => {
let base = 10 ^ ((locationChunks.length ?? 0) - index);
if (chunk === "*") score += 1 * base;
if (chunk.startsWith(":")) score += 2 * base;
if (chunk === locationChunks[index]) score += 3 * base;
});
return score;
};
const resolveRoutesPath = (routes: RouteData[]) =>
routes.reduce((pre, cur) => {
const path = trimSlash(cur.path ?? "");
return `${pre}${path ? SLASH + path : ""}`;
}, "") || "/";
const isLocationEnd = (locationPathname: string, routePath: string) => {
return split(locationPathname).length === split(routePath).length;
};
const matchRoute = (
locationPathname: string,
route: RouteData,
parentRouteChain: RouteData[] = []
) => {
const routeStack: RouteData[] = parentRouteChain;
let matchedRouteChains: RouteData[][] = [];
const walk = (route: RouteData) => {
const routeChain = [...routeStack, route];
const routePath = resolveRoutesPath(routeChain);
const isEnd = (route.children?.length ?? 0) === 0;
const result = testPath(locationPathname, routePath, isEnd);
if (result) {
if (isEnd) {
matchedRouteChains.push(routeChain);
} else {
const preChainSize = matchedRouteChains.length;
route.children?.forEach((childRoute) => {
routeStack.push(route);
walk(childRoute);
routeStack.pop();
});
const afterChainSize = matchedRouteChains.length;
if (
afterChainSize === preChainSize &&
isLocationEnd(locationPathname, routePath)
) {
matchedRouteChains.push(routeChain);
}
}
}
};
walk(route);
return matchedRouteChains;
};
export const matchRoutes = (
locationPathname: string,
routes: RouteData[],
parentRouteChain: RouteData[] = []
) => {
const matchedRouteChains: RouteData[][] = [];
routes.forEach((route) => {
matchedRouteChains.push(
...matchRoute(locationPathname, route, parentRouteChain)
);
});
matchedRouteChains.sort((a, b) => {
const result =
getScore(locationPathname, b) - getScore(locationPathname, a);
if (result !== 0) return result;
return b.length - a.length;
});
const finalChain = matchedRouteChains.at(0) ?? [];
if (finalChain) {
finalChain.forEach((route, index) => {
const subChain = finalChain.slice(0, index + 1);
const { params, pathname } = analysePath(
locationPathname,
resolveRoutesPath(subChain)
);
(route as MatchRoute).params = params;
(route as MatchRoute).pathname = pathname;
});
}
return finalChain;
};
export const resolvePath = (from: string, to: string) =>
new URL(to, `http://x${from}`).pathname;
================================================
FILE: packages/unis-router/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "build"
},
"include": ["src"]
}
================================================
FILE: packages/unis-router/vitest.config.ts
================================================
import { defineConfig } from "vitest/config";
import { unisPreset } from "@unis/vite-preset";
export default defineConfig({
plugins: [unisPreset()],
test: {
include: ["**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
coverage: {
reporter: ["text", "json", "html"],
},
},
});
================================================
FILE: packages/unis-transition/.gitignore
================================================
build/
dist/
================================================
FILE: packages/unis-transition/README.md
================================================
# Unis Transition
Transition component for unis inspired by `React Transition Group`.
## Install
```shell
npm i @unis/transition
```
## Usage
```javascript
import { useState } from '@unis/unis';
import { CSSTransition, TransitionGroup } from '@unis/transition'
const App = () => {
let [visible, setVisible] = useState(false);
const handleToggle = () => {
setVisible(!visible);
}
return () => (
toggle
hello
)
}
```
```css
.fade-appear {
opacity: 0;
}
.fade-appear-active {
opacity: 1;
transition: all 0.4s ease;
}
.fade-appear-done {
opacity: 1;
}
.fade-enter {
opacity: 0;
}
.fade-enter-active {
opacity: 1;
transition: all 0.4s ease;
}
.fade-enter-done {
opacity: 1;
}
.fade-exit {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
transition: all 0.4s ease;
}
.fade-exit-done {
opacity: 0;
}
```
Online [demo](https://stackblitz.com/edit/vitejs-vite-4cfy2b) here
## CSSTransition
Component API as close to `React Transition Group` as possible.
### in
Show the component.
type: `boolean`
default: `false`
### mountOnEnter
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.
type: `boolean`
default: `true`
### unmountOnExit
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.
type: `boolean`
default: `true`
### classNames
type:
```typescript
string | {
appear?: string,
appearActive?: string,
appearDone?: string,
enter?: string,
enterActive?: string,
enterDone?: string,
exit?: string,
exitActive?: string,
exitDone?: string,
}
```
default: `''`
for example `classNames="fade"` it will apply classes below
- `fade-appear`, `fade-appear-active`, `fade-appear-done`
- `fade-enter`, `fade-enter-active`, `fade-enter-done`
- `fade-exit`, `fade-exit-active`, `fade-exit-done`
### onEnter
Callback fired immediately after the 'enter' or 'appear' class is applied.
type: `Function(node: HtmlElement)`
### onEntering
Callback fired immediately after the 'enter-active' or 'appear-active' class is applied.
type: `Function(node: HtmlElement)`
### onEntered
Callback fired immediately after the 'enter' or 'appear' classes are removed and the done class is added to the DOM node.
type: `Function(node: HtmlElement)`
### onExit
Callback fired immediately after the 'exit' class is applied.
type: `Function(node: HtmlElement)`
### onExiting
Callback fired immediately after the 'exit-active' is applied.
type: `Function(node: HtmlElement)`
### onExited
Callback fired immediately after the 'exit' classes are removed and the exit-done class is added to the DOM node.
type: `Function(node?: HtmlElement)`
## TransitionGroup
Easy to use, just wrap on `CSSTransition` with key.
```javascript
{list.map((item, index) => (
{item.name}
))}
```
================================================
FILE: packages/unis-transition/package.json
================================================
{
"name": "@unis/transition",
"version": "0.1.0",
"description": "Unis transition component",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"typings": "dist/index.d.ts",
"scripts": {
"build": "rimraf build && rimraf dist && tsc -p tsconfig.json && rollup --config",
"build:dev": "cross-env NODE_ENV=development pnpm build"
},
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs"
}
},
"keywords": [
"transition",
"animation",
"unis"
],
"files": [
"dist"
],
"author": "anuoua",
"peerDependencies": {
"@unis/core": "workspace:^"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/anuoua/unis/issues"
},
"homepage": "https://github.com/anuoua/unis/tree/main/packages/unis-transition",
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.2",
"@unis/core": "workspace:^",
"cross-env": "^7.0.3",
"esbuild": "^0.17.15",
"rimraf": "^4.4.1",
"rollup": "^3.20.2",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0",
"rollup-plugin-reassign": "^1.0.3",
"typescript": "^5.0.3"
}
}
================================================
FILE: packages/unis-transition/rollup.config.mjs
================================================
import dts from "rollup-plugin-dts";
import esbuild from "rollup-plugin-esbuild";
import { defineConfig } from "rollup";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import { reassign } from "rollup-plugin-reassign";
import { unisFns } from "@unis/core";
const configGen = (format) =>
defineConfig({
input: "src/index.ts",
external: [/^@unis/],
output: [
{
dir: "dist",
entryFileNames: `index.${format === "esm" ? "mjs" : "js"}`,
format,
sourcemap: true,
},
],
plugins: [
nodeResolve(),
esbuild({
sourceMap: true,
target: "esnext",
jsx: "automatic",
jsxImportSource: "@unis/core",
}),
reassign({
include: ["**/*.(t|j)s?(x)"],
targetFns: {
"@unis/core": unisFns,
},
}),
],
});
const dtsRollup = () =>
defineConfig({
input: "build/index.d.ts",
output: [{ file: "dist/index.d.ts", format: "es" }],
plugins: [dts()],
});
const config = [configGen("cjs"), configGen("esm"), dtsRollup()];
export default config;
================================================
FILE: packages/unis-transition/src/CSSTransition.ts
================================================
import { findEls, use, useLayoutEffect, useProps } from "@unis/core";
import { uInstance } from "./hooks/uInstance";
import {
APPEARED,
APPEARING,
ENTERED,
ENTERING,
EXITED,
EXITING,
TransitionTimeout,
uTransition,
} from "./hooks/uTransition";
import { uWatch } from "./hooks/uWatch";
export interface TransitionProps {
children: JSX.Element;
classNames: string;
timeout?: TransitionTimeout;
in?: boolean;
mountOnEnter?: boolean;
unmountOnExit?: boolean;
appear?: boolean;
enter?: boolean;
onEnter?: (el: HTMLElement) => void;
onEntering?: (el: HTMLElement) => void;
onEntered?: (el: HTMLElement) => void;
onExit?: (el: HTMLElement) => void;
onExiting?: (el: HTMLElement) => void;
onExited?: (el?: HTMLElement) => void;
}
export const CSSTransition = (p: TransitionProps) => {
let {
children,
timeout,
in: inProp,
classNames,
unmountOnExit,
mountOnEnter,
appear,
enter,
onEnter,
onEntering,
onEntered,
onExit,
onExiting,
onExited,
} = useProps(p);
let [instance] = use(uInstance());
let { childrenState, status } = use(
uTransition(() => ({
in: inProp,
timeout,
enter,
unmountOnExit,
mountOnEnter,
appear,
}))
);
let currentChildren = use(() => (childrenState ? children : null));
let cls = use(() => ({
appear: `${classNames}-appear`,
appearActive: `${classNames}-appear-active`,
appearDone: `${classNames}-appear-done`,
enter: `${classNames}-enter`,
enterActive: `${classNames}-enter-active`,
enterDone: `${classNames}-enter-done`,
exit: `${classNames}-exit`,
exitActive: `${classNames}-exit-active`,
exitDone: `${classNames}-exit-done`,
}));
let el: HTMLElement | undefined;
const getElement = () => {
const els = findEls(instance, true).filter(
(el) => el instanceof HTMLElement
) as HTMLElement[];
return els[0] as HTMLElement | undefined;
};
const clearAll = () => {
if (!el) return;
el.classList.remove(
cls.appear,
cls.appearActive,
cls.appearDone,
cls.enter,
cls.enterActive,
cls.enterDone,
cls.exit,
cls.exitActive,
cls.exitDone
);
};
const entered = (appear: boolean) => {
if (!el) return;
clearAll();
el.classList.add(appear ? cls.appearDone : cls.enterDone);
onEntered?.(el);
};
const exited = () => {
if (el) {
clearAll();
el.classList.add(cls.exitDone);
}
onExited?.(el);
};
const entering = (reflow = false, apear: boolean) => {
if (!el) return;
clearAll();
el.classList.add(apear ? cls.appear : cls.enter);
onEnter?.(el);
reflow && forceReflow(el);
clearAll();
el.classList.add(apear ? cls.appearActive : cls.enterActive);
onEntering?.(el);
};
const exiting = (reflow = false) => {
if (!el) return;
clearAll();
el.classList.add(cls.exit);
onExit?.(el);
reflow && forceReflow(el);
clearAll();
el.classList.add(cls.exitActive);
onExiting?.(el);
};
useLayoutEffect(
() => {
el = getElement();
},
() => [status, inProp]
);
uWatch(
(current, previous) => {
switch (current) {
case APPEARING:
entering(previous !== EXITING, true);
break;
case APPEARED:
entered(true);
break;
case ENTERING:
entering(previous !== EXITING, false);
break;
case EXITING:
exiting(previous !== ENTERING);
break;
case ENTERED:
entered(false);
break;
case EXITED:
exited();
break;
}
},
() => [status]
);
return () => currentChildren;
};
const forceReflow = (el: HTMLElement) => el.scrollTop;
================================================
FILE: packages/unis-transition/src/TransitionGroup.ts
================================================
import { FiberNode, use, useEffect, useProps, useState } from "@unis/core";
import { CSSTransition } from "./CSSTransition";
export interface TransitionGroupProps {
children: JSX.Element | JSX.Element[];
}
export const TransitionGroup = (p: TransitionGroupProps) => {
let { children } = useProps(p);
let [transitionChildren, setTransitionChildren] = useState([]);
let flatChildren = use(() =>
([] as FiberNode[])
.concat(children as unknown as FiberNode)
.filter((child) => child.tag === CSSTransition)
);
let childrenKeys = use(() =>
flatChildren.map((child) => child.props.key).join(",")
);
const remove = (key: string) => {
setTransitionChildren(
transitionChildren.filter((child) => child.props.key !== key)
);
};
const handleChildren = () => {
const flatMap: Record = {};
flatChildren.forEach((child) => {
flatMap[child.props.key] = child;
});
const transitionMap: Record = {};
transitionChildren.forEach((child) => {
transitionMap[child.props.key] = child;
});
const newFlatChildren = flatChildren.map((child) => {
const existingNode = transitionMap[child.props.key];
if (existingNode) return existingNode;
const newChild = { ...child };
const originProps = newChild.props;
const onExited = newChild.props.onExited;
newChild.props = {
...originProps,
in: true,
onExited: (...args: unknown[]) => {
onExited?.(...args);
remove(originProps.key);
},
};
return newChild;
});
transitionChildren.forEach((child, index) => {
const newChild = { ...child };
if (!flatMap[newChild.props.key]) {
newChild.props = {
...newChild.props,
in: false,
};
newFlatChildren.splice(index, 0, newChild);
}
});
setTransitionChildren(newFlatChildren);
};
useEffect(
() => {
handleChildren();
},
() => [childrenKeys]
);
return () => transitionChildren;
};
================================================
FILE: packages/unis-transition/src/hooks/uInstance.ts
================================================
import { Fiber, use } from "@unis/core";
export const uInstance = () => {
let instance = use((WF: Fiber) => WF);
return () => [instance];
};
================================================
FILE: packages/unis-transition/src/hooks/uTransition.ts
================================================
import { use, useEffect, useState } from "@unis/core";
export const UNMOUNTED = "unmounted";
export const APPEARING = "appearing";
export const APPEARED = "appeared";
export const ENTERING = "entering";
export const ENTERED = "entered";
export const EXITING = "exiting";
export const EXITED = "exited";
export type TimeoutObject = {
appear?: number;
enter?: number;
exit?: number;
};
export type TransitionTimeout = number | TimeoutObject;
export interface uTransitionProps {
in?: boolean;
enter?: boolean;
unmountOnExit?: boolean;
mountOnEnter?: boolean;
appear?: boolean;
timeout?: TransitionTimeout;
}
export const uTransition = (optsFn: () => uTransitionProps) => {
let {
in: inProp = false,
enter = true,
unmountOnExit = true,
mountOnEnter = true,
timeout = 0,
appear = false,
} = use(optsFn);
let timer: number;
let mounted = false;
let realMountOnEnter = use(() => (unmountOnExit ? true : mountOnEnter));
let timeoutObject = use(() => {
if (typeof timeout === "number") {
return {
appear: timeout,
enter: timeout,
exit: timeout,
} as TimeoutObject;
} else {
const enter = timeout.enter ?? 0;
return {
appear: enter,
enter,
exit: timeout.exit ?? 0,
} as TimeoutObject;
}
});
let [childrenState, setChildrenState] = useState(
!realMountOnEnter ? true : inProp
);
let [initialStatus] = useState(
childrenState && appear && inProp
? enter
? APPEARING
: ENTERED
: UNMOUNTED
);
useEffect(
() => {
if (childrenState && initialStatus === APPEARING) {
setTimer(timeoutObject.appear!);
}
},
() => []
);
let [status, setStatus] = useState(initialStatus);
const switchChildren = () => {
if (status === ENTERING) setStatus(ENTERED);
if (status === EXITING) setStatus(EXITED);
if (status === APPEARING) setStatus(APPEARED);
setChildrenState(inProp ? true : !unmountOnExit);
};
function setTimer(statusTimeout: number) {
clearTimeout(timer);
timer = setTimeout(() => {
switchChildren();
}, statusTimeout);
}
useEffect(
() => {
if (!mounted) {
mounted = true;
return;
}
if (inProp) {
if (!enter) return setStatus(ENTERED);
setStatus(ENTERING);
setChildrenState(true);
setTimer(timeoutObject.enter!);
} else {
setStatus(EXITING);
setTimer(timeoutObject.exit!);
}
},
() => [inProp]
);
return () => ({
childrenState,
status,
});
};
================================================
FILE: packages/unis-transition/src/hooks/uUpdate.ts
================================================
import { useReducer } from "@unis/core";
export const uUpdate = () => {
let [, dispatch] = useReducer((a) => a + 1, 0);
const update = () => dispatch(undefined);
return () => [update];
};
================================================
FILE: packages/unis-transition/src/hooks/uWatch.ts
================================================
import { use, useLayoutEffect } from "@unis/core";
export const uWatch = (
handler: (currentValue: T, previousValue: T | undefined) => void,
depsFn: () => [T]
) => {
let [value] = use(depsFn);
let preValue: T | undefined = undefined;
useLayoutEffect(
() => {
handler(value, preValue);
preValue = value;
},
() => [value]
);
};
================================================
FILE: packages/unis-transition/src/index.ts
================================================
export * from "./CSSTransition";
export * from "./TransitionGroup";
export * from "./hooks/uTransition";
================================================
FILE: packages/unis-transition/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "build"
},
"include": ["src"]
}
================================================
FILE: packages/unis-vite-preset/.gitignore
================================================
build/
================================================
FILE: packages/unis-vite-preset/README.md
================================================
# Unis Vite Preset
Unis develop preset for vite.
## Install
```shell
npm add -D @unis/vite-preset
```
## Usage
vite.config.js
```javascript
import { defineConfig } from "vite";
import { unisPreset } from '@unis/vite-preset'
export default defineConfig({
plugins: [
unisPreset()
]
});
```
================================================
FILE: packages/unis-vite-preset/package.json
================================================
{
"name": "@unis/vite-preset",
"version": "0.1.0",
"description": "Unis vite preset",
"main": "build/index.js",
"module": "build/index.mjs",
"types": "build/index.d.ts",
"typings": "build/index.d.ts",
"scripts": {
"build": "rimraf build && rollup --config && tsc",
"build:dev": "cross-env NODE_ENV=development pnpm build"
},
"exports": {
".": {
"require": "./build/index.js",
"import": "./build/index.mjs"
}
},
"keywords": [
"vite",
"preset",
"unis"
],
"files": [
"build"
],
"author": "anuoua",
"peerDependencies": {
"@unis/core": "workspace:^",
"vite": "^4.2.1"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/anuoua/unis/issues"
},
"homepage": "https://github.com/anuoua/unis/tree/main/packages/unis-vite-preset",
"dependencies": {
"@callback-reassign/rollup-plugin": "^0.0.1"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^13.0.6",
"@unis/core": "workspace:^",
"cross-env": "^7.0.3",
"esbuild": "^0.13.13",
"rimraf": "^3.0.2",
"rollup": "^2.72.0",
"rollup-plugin-esbuild": "^4.6.0",
"typescript": "^4.4.4",
"vite": "^4.2.1"
}
}
================================================
FILE: packages/unis-vite-preset/rollup.config.js
================================================
import { defineConfig } from "rollup";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import esbuild from "rollup-plugin-esbuild";
const configGen = (format) =>
defineConfig({
input: "src/index.ts",
external: [/^@unis/, "@callback-reassign/rollup-plugin"],
output: [
{
dir: "build",
entryFileNames: `index.${format === "esm" ? "mjs" : "js"}`,
format,
sourcemap: true,
},
],
plugins: [
nodeResolve(),
esbuild({
sourceMap: true,
minify: process.env.NODE_ENV === "development" ? false : true,
target: "esnext",
}),
],
});
const config = [configGen("cjs"), configGen("esm")];
export default config;
================================================
FILE: packages/unis-vite-preset/src/index.ts
================================================
import type { PluginOption } from "vite";
import { reassign } from "@callback-reassign/rollup-plugin";
import { unisFns } from "@unis/core";
export function unisPreset(): PluginOption[] {
return [
{
name: "unis-preset",
enforce: "pre",
config(config) {
return {
esbuild: {
jsx: "automatic",
jsxImportSource: "@unis/core",
...config.esbuild,
},
};
},
},
reassign({
include: ["**/*.(t|j)s?(x)"],
targetFns: {
"@unis/core": unisFns,
},
}) as PluginOption,
];
}
================================================
FILE: packages/unis-vite-preset/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": true,
"declarationMap": false,
"outDir": "build"
},
"include": ["src"]
}
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- "packages/**"
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"lib": [
"DOM",
"ESNext"
],
"jsx": "react-jsx",
"jsxImportSource": "@unis/core",
"module": "ESNext",
"moduleResolution": "node",
"declaration": true,
"emitDeclarationOnly": true,
"sourceMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noUnusedLocals": true,
"skipLibCheck": false
},
}