Compare commits

...

13 Commits

Author SHA1 Message Date
iandxssxx 4608422c0a feat: publish (feat: add lifecycles and watch) 2024-04-10 23:00:49 -04:00
iandxssxx 325f4c406a feat: add lifecycles and watch 2024-04-10 22:59:55 -04:00
iandxssxx 1f4b164952 refactor: publish new version 2024-04-09 21:58:08 -04:00
iandxssxx 2f9d3737db feat: add changeset and changed package name 2024-04-09 03:28:10 -04:00
iandxssxx ef4126b767 Merge branch 'API-2.0' into api2/for 2024-04-07 23:17:00 -04:00
陈超涛 b7756e9732
!171 fix: this patcher
Merge pull request !171 from Hoikan/API-2.0-4-8
2024-04-08 03:16:05 +00:00
Hoikan bf1bd09721 refactor: add this with thisPatcher.ts 2024-04-08 11:11:46 +08:00
iandxssxx d6ba039445 fix: import package from workspace 2024-04-07 23:09:39 -04:00
iandxssxx 4467bdae73 feat: add for unit parsing 2024-04-07 23:09:21 -04:00
iandxssxx 627a8b7785 feat: init benchmark demo 2024-04-07 23:08:50 -04:00
iandxssxx 109746acef refactor: gitignore add history 2024-04-07 23:08:39 -04:00
Hoikan d599b36eaa !169 feat: inula 2.0 init
* feat: update doc
* feat: inula next
2024-04-03 08:55:27 +00:00
Hoikan 83c80341dc !168 inula api2.0
* feat: inula-next init
* feat: v2 init
* feat(class-transform): add watch decorator
* feat(class-transform): update docs
* feat(class-transform): init
2024-04-03 08:41:11 +00:00
160 changed files with 15284 additions and 49 deletions

8
.changeset/README.md Normal file
View File

@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

11
.changeset/config.json Normal file
View File

@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": ["create-inula", "openinula", "inula-cli", "inula-dev-tools", "inula-intl", "inula-request", "inula-router", "inula-vite-app", "inula-webpack-app"]
}

5
.gitignore vendored
View File

@ -1,10 +1,11 @@
/node_modules
node_modules
.idea
.vscode
package-lock.json
pnpm-lock.yaml
/packages/**/node_modules
/packages/inula-cli/lib
build
/packages/inula-router/connectRouter
/packages/inula-router/router
dist
.history

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Inula-next</title>
</head>
<body>
<div id="main"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -0,0 +1,25 @@
{
"name": "dev",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@babel/standalone": "^7.22.4",
"@openinula/next": "workspace:*",
"@iandx/easy-css": "^0.10.14",
"babel-preset-inula-next": "workspace:*"
},
"devDependencies": {
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vite-plugin-inula-next": "workspace:*"
},
"keywords": [
"dlight.js"
]
}

View File

@ -0,0 +1,93 @@
import { render, View } from '@openinula/next';
let idCounter = 1;
const adjectives = ['pretty', 'large', 'big', 'small', 'tall', 'short', 'long', 'handsome', 'plain', 'quaint', 'clean', 'elegant', 'easy', 'angry', 'crazy', 'helpful', 'mushy', 'odd', 'unsightly', 'adorable', 'important', 'inexpensive', 'cheap', 'expensive', 'fancy'];
const colours = ['red', 'yellow', 'blue', 'green', 'pink', 'brown', 'purple', 'brown', 'white', 'black', 'orange'];
const nouns = ['table', 'chair', 'house', 'bbq', 'desk', 'car', 'pony', 'cookie', 'sandwich', 'burger', 'pizza', 'mouse', 'keyboard'];
function _random(max) {
return Math.round(Math.random() * 1000) % max;
}
function buildData(count) {
const data = new Array(count);
for (let i = 0; i < count; i++) {
data[i] = {
id: idCounter++,
label: `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`
};
}
return data;
}
function Button ({ id, text, fn }) {
return (
<div class='col-sm-6 smallpad'>
<button id={ id } class='btn btn-primary btn-block' type='button' onClick={ fn }>{ text }</button>
</div>
);
}
function App () {
let data = [];
let selected = null;
function run() {
data = buildData(1000);
}
function runLots() {
data = buildData(10000);
}
function add() {
data.push(...buildData(1000));
}
function update() {
for (let i = 0; i < data.length; i += 10) {
data[i].label += ' !!!';
}
}
function swapRows() {
if (data.length > 998) {
[data[1], data[998]] = [data[998], data[1]];
}
}
function clear() {
data = [];
}
function remove(id) {
data = data.filter(d => d.id !== id);
}
function select(id) {
selected = id;
}
return (
<div class='container'>
<div class='jumbotron'><div class='row'>
<div class='col-md-6'><h1>Inula-next Keyed</h1></div>
<div class='col-md-6'><div class='row'>
<Button id='run' text='Create 1,000 rows' fn={ run } />
<Button id='runlots' text='Create 10,000 rows' fn={ runLots } />
<Button id='add' text='Append 1,000 rows' fn={ add } />
<Button id='update' text='Update every 10th row' fn={ update } />
<Button id='clear' text='Clear' fn={ clear } />
<Button id='swaprows' text='Swap Rows' fn={ swapRows } />
</div></div>
</div></div>
<table class='table table-hover table-striped test-data'><tbody>
<for array={ data } item={ { id, label } } key={ id }>
<tr class={ selected === id ? 'danger': '' }>
<td class='col-md-1' textContent={ id } />
<td class='col-md-4'><a onClick={select.bind(this, id)} textContent={ label } /></td>
<td class='col-md-1'><a onClick={remove.bind(this, id)}><span class='glyphicon glyphicon-remove' aria-hidden="true" /></a></td>
<td class='col-md-6'/>
</tr>
</for>
</tbody></table>
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden="true" />
</div>
);
}
render('main', App);

View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vite';
import inula from 'vite-plugin-inula-next';
export default defineConfig({
server: {
port: 4320,
},
base: '',
optimizeDeps: {
disabled: true,
},
plugins: [inula({ files: '**/*.{tsx,jsx}' })],
});

8
demos/v2/CHANGELOG.md Normal file
View File

@ -0,0 +1,8 @@
# dev
## 0.0.1
### Patch Changes
- Updated dependencies [2f9d373]
- babel-preset-inula-next@0.0.2

12
demos/v2/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Dlight.JS</title>
<link rel="stylesheet" href="/src/App.css"/>
</head>
<body>
<div id="main"></div>
<script type="module" src="/src/App.tsx"></script>
</body>
</html>

25
demos/v2/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "dev",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@babel/standalone": "^7.22.4",
"@openinula/next": "workspace:*",
"@iandx/easy-css": "^0.10.14",
"babel-preset-inula-next": "workspace:*"
},
"devDependencies": {
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vite-plugin-inula-next": "workspace:*"
},
"keywords": [
"dlight.js"
]
}

3
demos/v2/src/App.css Normal file
View File

@ -0,0 +1,3 @@
.ok {
color: var(--color-ok);
}

17
demos/v2/src/App.tsx Normal file
View File

@ -0,0 +1,17 @@
import { View, render } from '@openinula/next';
function MyComp() {
let count = 0;
const db = count * 2;
return (
<>
<h1 className="123">Hello dlight fn comp</h1>
<section>
count: {count}, double is: {db}
<button onClick={() => (count += 1)}>Add</button>
</section>
</>
);
}
render('main', MyComp);

143
demos/v2/src/App.view.tsx Normal file
View File

@ -0,0 +1,143 @@
// @ts-nocheck
import {
Children,
Content,
Main,
Model,
Prop,
View,
Watch,
button,
div,
input,
insertChildren,
use,
render,
} from '@openinula/next';
// @ts-ignore
function Button({ children, onClick }) {
return (
<button
onClick={onClick}
style={{
color: 'white',
backgroundColor: 'green',
border: 'none',
padding: '5px 10px',
marginRight: '10px',
borderRadius: '4px',
}}
>
{children}
</button>
);
}
function ArrayModification() {
const arr = [];
willMount(() => {});
return (
<section>
<h1>ArrayModification</h1>
{arr.join(',')}
<button onClick={() => arr.push(arr.length)}>Add item</button>
</section>
);
}
function Counter() {
let count = 0;
const doubleCount = count * 2; // 当count变化时doubleCount自动更新
// 当count变化时watch会自动执行
watch(() => {
uploadToServer(count);
console.log(`count has changed: ${count}`);
});
// 只有在init的时候执行一次
console.log(`Counter willMount with count ${count}`);
// 在elements被挂载到DOM之后执行
didMount(() => {
console.log(`Counter didMount with count ${count}`);
});
return (
<section>
count: {count}, double is: {doubleCount}
<button onClick={() => (count ++)}>Add</button>
</section>
);
}
function Counter() {
let count = 0;
const doubleCount = count * 2; // 当count变化时doubleCount自动更新
uploadToServer(count); // 当count变化时uploadToServer会自动执行
console.log(`count has changed: ${count}`); // 当count变化时console.log会自动执行
// 只有在init的时候执行一次
willMount(() => {
console.log(`Counter willMount with count ${count}`);
});
// 在elements被挂载到DOM之后执行
didMount(() => {
console.log(`Counter didMount with count ${count}`);
});
return (
<section>
count: {count}, double is: {doubleCount}
<button onClick={() => (count ++)}>Add</button>
</section>
);
}
function MyComp() {
let count = 0;
{
console.log(count);
const i = count * 2;
console.log(i);
}
console.log(count);
const i = count * 2;
console.log(i);
const XX = () => {
};
return (
<>
<h1 className="123">Hello dlight fn comp</h1>
<section>
count: {count}, double is: {db}
<button onClick={() => (count += 1)}>Add</button>
</section>
<Button onClick={() => alert(count)}>Alter count</Button>
<ConditionalRendering count={count} />
<ArrayModification />
</>
);
}
function ConditionalRendering({ count }) {
return (
<section>
<h1>Condition</h1>
<if cond={count > 1}>{count} is bigger than is 1</if>
<else-if cond={count === 1}>{count} is equal to 1</else-if>
<else>{count} is smaller than 1</else>
</section>
);
}
render('main', MyComp);

20
demos/v2/tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"jsx": "preserve",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"experimentalDecorators": true
},
"ts-node": {
"esm": true
}
}

13
demos/v2/vite.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vite';
import inula from 'vite-plugin-inula-next';
export default defineConfig({
server: {
port: 4320,
},
base: '',
optimizeDeps: {
disabled: true,
},
plugins: [inula({ files: '**/*.{ts,js,tsx,jsx}' })],
});

View File

@ -13,6 +13,7 @@
"build:inula-intl": "pnpm -F inula-intl build",
"build:inula-request": "pnpm -F inula-request build",
"build:inula-router": "pnpm -F inula-router build",
"build:transpiler": "pnpm --filter './packages/transpiler/*' run build",
"commitlint": "commitlint --config commitlint.config.js -e",
"postinstall": "husky install"
},
@ -80,5 +81,9 @@
"engines": {
"node": ">=10.x",
"npm": ">=7.x"
},
"dependencies": {
"@changesets/cli": "^2.27.1",
"changeset": "^0.2.6"
}
}

View File

@ -54,7 +54,7 @@ inula-cli的推荐目录结构如下
│ └── inula-cli
│ ├── lib
├── mock // mock目录
│ └── mock.ts
│ └── transform.ts
├── src // 项目源码目录
│ ├── pages
│ │ ├── index.less
@ -178,10 +178,10 @@ inula-cli的所有功能都围绕插件展开插件可以很方便地让用
inula-cli支持用户集成已发布在npm仓库的插件用户可以按需安装并运行这些插件。
安装可以通过npm安装这里以插件@inula/add为例
安装可以通过npm安装这里以插件@openinula/add为例
```shell
npm i --save-dev @inula/add
npm i --save-dev @openinula/add
```
如果需要运行插件,需要在配置文件中配置对应的插件路径
@ -191,7 +191,7 @@ npm i --save-dev @inula/add
export default {
...
plugins:["@inula/add"]
plugins:["@openinula/add"]
}
```

View File

@ -14,4 +14,3 @@
*/
declare module 'crequire';

View File

@ -57,7 +57,7 @@ export default (api: API) => {
api.applyHook({ name: 'afterStartDevServer' });
});
} else {
api.logger.error('Can\'t find config');
api.logger.error("Can't find config");
}
break;
case 'vite':
@ -70,7 +70,7 @@ export default (api: API) => {
server.printUrls();
});
} else {
api.logger.error('Can\'t find config');
api.logger.error("Can't find config");
}
break;
default:

View File

@ -33,7 +33,7 @@ export default (api: API) => {
args._.shift();
}
if (args._.length === 0) {
api.logger.warn('Can\'t find any generate options.');
api.logger.warn("Can't find any generate options.");
return;
}

View File

@ -16,10 +16,7 @@
const { preset } = require('./jest.config');
module.exports = {
presets: [
[
'@babel/preset-env',
{ targets: { node: 'current' } },
],
['@babel/preset-env', { targets: { node: 'current' } }],
['@babel/preset-typescript'],
[
'@babel/preset-react',

View File

@ -40,7 +40,7 @@ export default {
{
file: path.resolve(output, 'intl.esm-browser.js'),
format: 'esm',
}
},
],
plugins: [
nodeResolve({

View File

@ -14,11 +14,11 @@
*/
const body: Record<string, any> = {
doubleapos: { match: '\'\'', value: () => '\'' },
doubleapos: { match: "''", value: () => "'" },
quoted: {
lineBreaks: true,
match: /'[{}#](?:[^]*?[^'])?'(?!')/u,
value: src => src.slice(1, -1).replace(/''/g, '\''),
value: src => src.slice(1, -1).replace(/''/g, "'"),
},
argument: {
lineBreaks: true,

View File

@ -90,19 +90,19 @@ describe('I18n', () => {
});
it('._ allow escaping syntax characters', () => {
const messages = {
'My \'\'name\'\' is \'{name}\'': 'Mi \'\'nombre\'\' es \'{name}\'',
"My ''name'' is '{name}'": "Mi ''nombre'' es '{name}'",
};
const i18n = new I18n({
locale: 'es',
messages: { es: messages },
});
expect(i18n.formatMessage('My \'\'name\'\' is \'{name}\'')).toEqual('Mi \'nombre\' es {name}');
expect(i18n.formatMessage("My ''name'' is '{name}'")).toEqual("Mi 'nombre' es {name}");
});
it('._ should format message from catalog', function () {
const messages = {
Hello: 'Salut',
id: 'Je m\'appelle {name}',
id: "Je m'appelle {name}",
};
const i18n = new I18n({
locale: 'fr',
@ -110,7 +110,7 @@ describe('I18n', () => {
});
expect(i18n.locale).toEqual('fr');
expect(i18n.formatMessage('Hello')).toEqual('Salut');
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual('Je m\'appelle Fred');
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual("Je m'appelle Fred");
});
it('should return the formatted date and time', () => {

View File

@ -43,7 +43,7 @@ describe('eventEmitter', () => {
expect(listener).not.toBeCalled();
});
it('should do nothing when even doesn\'t exist', () => {
it("should do nothing when even doesn't exist", () => {
const unknown = jest.fn();
const emitter = new EventEmitter();

View File

@ -0,0 +1,38 @@
{
"name": "@openinula/store",
"version": "0.0.0",
"description": "DLight shared store",
"author": {
"name": "IanDx",
"email": "iandxssxx@gmail.com"
},
"keywords": [
"dlight.js"
],
"license": "MIT",
"files": [
"dist"
],
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"build": "tsup --sourcemap"
},
"devDependencies": {
"tsup": "^6.5.0",
"typescript": "^5.3.2"
},
"tsup": {
"entry": [
"src/index.ts"
],
"format": [
"esm"
],
"clean": true,
"dts": true,
"minify": true
}
}

View File

@ -0,0 +1 @@
export const Store = {};

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
}

View File

@ -0,0 +1,2 @@
# DLight Main Package
See the website's documentations for usage.

View File

@ -0,0 +1,41 @@
{
"name": "@openinula/next",
"version": "0.0.1",
"author": {
"name": "IanDx",
"email": "iandxssxx@gmail.com"
},
"keywords": [
"inula"
],
"license": "MIT",
"files": [
"dist",
"README.md"
],
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"build": "tsup --sourcemap && cp src/index.d.ts dist/ && cp -r src/types dist/"
},
"dependencies": {
"csstype": "^3.1.3",
"@openinula/store": "workspace:*"
},
"devDependencies": {
"tsup": "^6.5.0"
},
"tsup": {
"entry": [
"src/index.js"
],
"format": [
"cjs",
"esm"
],
"clean": true,
"minify": true
}
}

View File

@ -0,0 +1,367 @@
import { DLNode, DLNodeType } from './DLNode';
import { forwardHTMLProp } from './HTMLNode';
import { DLStore, cached } from './store';
export class CompNode extends DLNode {
/**
* @brief Constructor, Comp type
* @internal
* * key - private property key
* * $$key - dependency number, e.g. 0b1, 0b10, 0b100
* * $s$key - set of properties that depend on this property
* * $p$key - exist if this property is a prop
* * $e$key - exist if this property is an env
* * $en$key - exist if this property is an env, and it's the innermost env that contains this env
* * $w$key - exist if this property is a watcher
* * $f$key - a function that returns the value of this property, called when the property's dependencies change
* * _$children - children nodes of type PropView
* * _$contentKey - the key key of the content prop
* * _$forwardProps - exist if this node is forwarding props
* * _$forwardPropsId - the keys of the props that this node is forwarding, collected in _$setForwardProp
* * _$forwardPropsSet - contain all the nodes that are forwarding props to this node, collected with _$addForwardProps
*/
constructor() {
super(DLNodeType.Comp);
}
/**
* @brief Init function, called explicitly in the subclass's constructor
* @param props - Object containing properties
* @param content - Content to be used
* @param children - Child nodes
* @param forwardPropsScope - Scope for forwarding properties
*/
_$init(props, content, children, forwardPropsScope) {
this._$notInitd = true;
// ---- Forward props first to allow internal props to override forwarded props
if (forwardPropsScope) forwardPropsScope._$addForwardProps(this);
if (content) this._$setContent(() => content[0], content[1]);
if (props)
props.forEach(([key, value, deps]) => {
if (key === 'props') return this._$setProps(() => value, deps);
this._$setProp(key, () => value, deps);
});
if (children) this._$children = children;
// ---- Add envs
DLStore.global.DLEnvStore &&
Object.entries(DLStore.global.DLEnvStore.envs).forEach(([key, [value, envNode]]) => {
if (key === '_$catchable') {
this._$catchable = value;
return;
}
if (!(`$e$${key}` in this)) return;
envNode.addNode(this);
this._$initEnv(key, value, envNode);
});
const willCall = () => {
this._$callUpdatesBeforeInit();
this.didMount && DLNode.addDidMount(this, this.didMount.bind(this));
this.willUnmount && DLNode.addWillUnmount(this, this.willUnmount.bind(this));
DLNode.addDidUnmount(this, this._$setUnmounted.bind(this));
this.didUnmount && DLNode.addDidUnmount(this, this.didUnmount.bind(this));
this.willMount?.();
this._$nodes = this.Body?.() ?? [];
};
if (this._$catchable) {
this._$catchable(willCall)();
if (this._$update) this._$update = this._$catchable(this._$update.bind(this));
this._$updateDerived = this._$catchable(this._$updateDerived.bind(this));
delete this._$catchable;
} else {
willCall();
}
}
_$setUnmounted() {
this._$unmounted = true;
}
/**
* @brief Call updates manually before the node is mounted
*/
_$callUpdatesBeforeInit() {
const protoProps = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
const ownProps = Object.getOwnPropertyNames(this);
const allProps = [...protoProps, ...ownProps];
allProps.forEach(key => {
// ---- Run watcher
if (key.startsWith('$w$')) return this[key.slice(3)]();
// ---- Run model update
if (key.startsWith('$md$')) {
const realKey = key.slice(4);
this[realKey] = this[realKey]();
return;
}
// ---- Run derived value
if (key.startsWith('$f$')) {
const realKey = key.slice(3);
this[realKey] = this[key];
this._$updateDerived(realKey);
}
});
delete this._$notInitd;
}
/**
* @brief Set all the props to forward
* @param key
* @param value
* @param deps
*/
_$setPropToForward(key, value, deps) {
this._$forwardPropsSet.forEach(node => {
const isContent = key === '_$content';
if (node._$dlNodeType === DLNodeType.Comp) {
if (isContent) node._$setContent(() => value, deps);
else node._$setProp(key, () => value, deps);
return;
}
if (node instanceof HTMLElement) {
if (isContent) key = 'textContent';
forwardHTMLProp(node, key, () => value, deps);
}
});
}
/**
* @brief Define forward props
* @param key
* @param value
*/
_$setForwardProp(key, valueFunc, deps) {
const notInitd = '_$notInitd' in this;
if (!notInitd && this._$cache(key, deps)) return;
const value = valueFunc();
if (key === '_$content' && this._$contentKey) {
this[this._$contentKey] = value;
this._$updateDerived(this._$contentKey);
}
this[key] = value;
this._$updateDerived(key);
if (notInitd) this._$forwardPropsId.push(key);
else this._$setPropToForward(key, value, deps);
}
/**
* @brief Add a node to the set of nodes that are forwarding props to this node and init these props
* @param node
*/
_$addForwardProps(node) {
this._$forwardPropsSet.add(node);
this._$forwardPropsId.forEach(key => {
this._$setPropToForward(key, this[key], []);
});
DLNode.addWillUnmount(node, this._$forwardPropsSet.delete.bind(this._$forwardPropsSet, node));
}
/**
* @brief Cache the deps and return true if the deps are the same as the previous deps
* @param key
* @param deps
* @returns
*/
_$cache(key, deps) {
if (!deps || !deps.length) return false;
const cacheKey = `$cc$${key}`;
if (cached(deps, this[cacheKey])) return true;
this[cacheKey] = deps;
return false;
}
/**
* @brief Set the content prop, the key is stored in _$contentKey
* @param value
*/
_$setContent(valueFunc, deps) {
if ('_$forwardProps' in this) return this._$setForwardProp('_$content', valueFunc, deps);
const contentKey = this._$contentKey;
if (!contentKey) return;
if (this._$cache(contentKey, deps)) return;
this[contentKey] = valueFunc();
this._$updateDerived(contentKey);
}
/**
* @brief Set a prop directly, if this is a forwarded prop, go and init forwarded props
* @param key
* @param value
* @param deps
*/
_$setProp(key, valueFunc, deps) {
if ('_$forwardProps' in this) return this._$setForwardProp(key, valueFunc, deps);
if (!(`$p$${key}` in this)) {
console.warn(`[${key}] is not a prop in ${this.constructor.name}`);
return;
}
if (this._$cache(key, deps)) return;
this[key] = valueFunc();
this._$updateDerived(key);
}
_$setProps(valueFunc, deps) {
if (this._$cache('props', deps)) return;
const props = valueFunc();
if (!props) return;
Object.entries(props).forEach(([key, value]) => {
this._$setProp(key, () => value, []);
});
}
/**
* @brief Init an env, put the corresponding innermost envNode in $en$key
* @param key
* @param value
* @param envNode
*/
_$initEnv(key, value, envNode) {
this[key] = value;
this[`$en$${key}`] = envNode;
}
// ---- Update functions
/**
* @brief Update an env, called in EnvNode._$update
* @param key
* @param value
* @param envNode
*/
_$updateEnv(key, value, envNode) {
if (!(`$e$${key}` in this)) return;
if (envNode !== this[`$en$${key}`]) return;
this[key] = value;
this._$updateDerived(key);
}
/**
* @brief Update a prop
*/
_$ud(exp, key) {
this._$updateDerived(key);
return exp;
}
/**
* @brief Update properties that depend on this property
* @param key
*/
_$updateDerived(key) {
if ('_$notInitd' in this) return;
this[`$s$${key}`]?.forEach(k => {
if (`$w$${k}` in this) {
// ---- Watcher
this[k](key);
} else if (`$md$${k}` in this) {
this[k]._$update();
} else {
// ---- Regular derived value
this[k] = this[`$f$${k}`];
}
});
// ---- "trigger-view"
this._$updateView(key);
}
_$updateView(key) {
if (this._$modelCallee) return this._$updateModelCallee();
if (!('_$update' in this)) return;
const depNum = this[`$$${key}`];
if (!depNum) return;
// ---- Collect all depNums that need to be updated
if ('_$depNumsToUpdate' in this) {
this._$depNumsToUpdate.push(depNum);
} else {
this._$depNumsToUpdate = [depNum];
// ---- Update in the next microtask
Promise.resolve().then(() => {
// ---- Abort if unmounted
if (this._$unmounted) return;
const depNums = this._$depNumsToUpdate;
if (depNums.length > 0) {
const depNum = depNums.reduce((acc, cur) => acc | cur, 0);
this._$update(depNum);
}
delete this._$depNumsToUpdate;
});
}
}
_$updateModelCallee() {
if ('_$depNumsToUpdate' in this) return;
this._$depNumsToUpdate = true;
// ---- Update in the next microtask
Promise.resolve().then(() => {
// ---- Abort if unmounted
if (this._$unmounted) return;
this._$modelCallee._$updateDerived(this._$modelKey);
delete this._$depNumsToUpdate;
});
}
/**
* @brief Update all props and content of the model
*/
static _$updateModel(model, propsFunc, contentFunc) {
// ---- Suppress update because top level update will be performed
// directly by the state variable in the model callee, which will
// trigger the update of the model
const props = propsFunc() ?? {};
const collectedProps = props.s ?? [];
props.m?.forEach(([props, deps]) => {
Object.entries(props).forEach(([key, value]) => {
collectedProps.push([key, value, deps]);
});
});
collectedProps.forEach(([key, value, deps]) => {
model._$setProp(key, () => value, deps);
});
const content = contentFunc();
if (content) model._$setContent(() => content[0], content[1]);
}
static _$releaseModel() {
delete this._$modelCallee;
}
/**
* @brief Inject Dlight model in to a property
* @param ModelCls
* @param props { m: [props, deps], s: [key, value, deps] }
* @param content
* @param key
* @returns
*/
_$injectModel(ModelCls, propsFunc, contentFunc, key) {
const props = propsFunc() ?? {};
const collectedProps = props.s ?? [];
props.m?.forEach(([props, deps]) => {
Object.entries(props).forEach(([key, value]) => {
collectedProps.push([key, value, deps]);
});
});
const model = new ModelCls();
model._$init(collectedProps, contentFunc(), null, null);
model._$modelCallee = this;
model._$modelKey = key;
model._$update = CompNode._$updateModel.bind(null, model, propsFunc, contentFunc);
return model;
}
}
// ---- @View -> class Comp extends View
export const View = CompNode;
export const Model = CompNode;
/**
* @brief Run all update functions given the key
* @param dlNode
* @param key
*/
export function update(dlNode, key) {
dlNode._$updateDerived(key);
}

View File

@ -0,0 +1,209 @@
import { DLStore } from './store';
export const DLNodeType = {
Comp: 0,
For: 1,
Cond: 2,
Env: 3,
Exp: 4,
Snippet: 5,
Try: 6,
};
export class DLNode {
/**
* @brief Node type: HTML, Text, Custom, For, If, Env, Expression
*/
_$dlNodeType;
/**
* @brief Constructor
* @param nodeType
*/
constructor(nodeType) {
this._$dlNodeType = nodeType;
}
/**
* @brief Node element
* Either one real element for HTMLNode and TextNode
* Or an array of DLNode for CustomNode, ForNode, IfNode, EnvNode, ExpNode
*/
get _$el() {
return DLNode.toEls(this._$nodes);
}
/**
* @brief Loop all child DLNodes to get all the child elements
* @param nodes
* @returns HTMLElement[]
*/
static toEls(nodes) {
const els = [];
this.loopShallowEls(nodes, el => {
els.push(el);
});
return els;
}
// ---- Loop nodes ----
/**
* @brief Loop all elements shallowly,
* i.e., don't loop the child nodes of dom elements and only call runFunc on dom elements
* @param nodes
* @param runFunc
*/
static loopShallowEls(nodes, runFunc) {
const stack = [...nodes].reverse();
while (stack.length > 0) {
const node = stack.pop();
if (!('_$dlNodeType' in node)) runFunc(node);
else node._$nodes && stack.push(...[...node._$nodes].reverse());
}
}
/**
* @brief Add parentEl to all nodes until the first element
* @param nodes
* @param parentEl
*/
static addParentEl(nodes, parentEl) {
nodes.forEach(node => {
if ('_$dlNodeType' in node) {
node._$parentEl = parentEl;
node._$nodes && DLNode.addParentEl(node._$nodes, parentEl);
}
});
}
// ---- Flow index and add child elements ----
/**
* @brief Get the total count of dom elements before the stop node
* @param nodes
* @param stopNode
* @returns total count of dom elements
*/
static getFlowIndexFromNodes(nodes, stopNode) {
let index = 0;
const stack = [...nodes].reverse();
while (stack.length > 0) {
const node = stack.pop();
if (node === stopNode) break;
if ('_$dlNodeType' in node) {
node._$nodes && stack.push(...[...node._$nodes].reverse());
} else {
index++;
}
}
return index;
}
/**
* @brief Given an array of nodes, append them to the parentEl
* 1. If nextSibling is provided, insert the nodes before the nextSibling
* 2. If nextSibling is not provided, append the nodes to the parentEl
* @param nodes
* @param parentEl
* @param nextSibling
* @returns Added element count
*/
static appendNodesWithSibling(nodes, parentEl, nextSibling) {
if (nextSibling) return this.insertNodesBefore(nodes, parentEl, nextSibling);
return this.appendNodes(nodes, parentEl);
}
/**
* @brief Given an array of nodes, append them to the parentEl using the index
* 1. If the index is the same as the length of the parentEl.childNodes, append the nodes to the parentEl
* 2. If the index is not the same as the length of the parentEl.childNodes, insert the nodes before the node at the index
* @param nodes
* @param parentEl
* @param index
* @param length
* @returns Added element count
*/
static appendNodesWithIndex(nodes, parentEl, index, length) {
length = length ?? parentEl.childNodes.length;
if (length !== index) return this.insertNodesBefore(nodes, parentEl, parentEl.childNodes[index]);
return this.appendNodes(nodes, parentEl);
}
/**
* @brief Insert nodes before the nextSibling
* @param nodes
* @param parentEl
* @param nextSibling
* @returns Added element count
*/
static insertNodesBefore(nodes, parentEl, nextSibling) {
let count = 0;
this.loopShallowEls(nodes, el => {
parentEl.insertBefore(el, nextSibling);
count++;
});
return count;
}
/**
* @brief Append nodes to the parentEl
* @param nodes
* @param parentEl
* @returns Added element count
*/
static appendNodes(nodes, parentEl) {
let count = 0;
this.loopShallowEls(nodes, el => {
parentEl.appendChild(el);
count++;
});
return count;
}
// ---- Lifecycle ----
/**
* @brief Add willUnmount function to node
* @param node
* @param func
*/
static addWillUnmount(node, func) {
const willUnmountStore = DLStore.global.WillUnmountStore;
const currentStore = willUnmountStore[willUnmountStore.length - 1];
// ---- If the current store is empty, it means this node is not mutable
if (!currentStore) return;
currentStore.push(func.bind(null, node));
}
/**
* @brief Add didUnmount function to node
* @param node
* @param func
*/
static addDidUnmount(node, func) {
const didUnmountStore = DLStore.global.DidUnmountStore;
const currentStore = didUnmountStore[didUnmountStore.length - 1];
// ---- If the current store is empty, it means this node is not mutable
if (!currentStore) return;
currentStore.push(func.bind(null, node));
}
/**
* @brief Add didUnmount function to global store
* @param func
*/
static addDidMount(node, func) {
if (!DLStore.global.DidMountStore) DLStore.global.DidMountStore = [];
DLStore.global.DidMountStore.push(func.bind(null, node));
}
/**
* @brief Run all didMount functions and reset the global store
*/
static runDidMount() {
const didMountStore = DLStore.global.DidMountStore;
if (!didMountStore || didMountStore.length === 0) return;
for (let i = didMountStore.length - 1; i >= 0; i--) {
didMountStore[i]();
}
DLStore.global.DidMountStore = [];
}
}

View File

@ -0,0 +1,103 @@
import { DLNode, DLNodeType } from './DLNode';
import { DLStore, cached } from './store';
export class EnvStoreClass {
constructor() {
this.envs = {};
this.currentEnvNodes = [];
}
/**
* @brief Add a node to the current env and merge envs
* @param node - The node to add
*/
addEnvNode(node) {
this.currentEnvNodes.push(node);
this.mergeEnvs();
}
/**
* @brief Replace the current env with the given nodes and merge envs
* @param nodes - The nodes to replace the current environment with
*/
replaceEnvNodes(nodes) {
this.currentEnvNodes = nodes;
this.mergeEnvs();
}
/**
* @brief Remove the last node from the current env and merge envs
*/
removeEnvNode() {
this.currentEnvNodes.pop();
this.mergeEnvs();
}
/**
* @brief Merge all the envs in currentEnvNodes, inner envs override outer envs
*/
mergeEnvs() {
this.envs = {};
this.currentEnvNodes.forEach(envNode => {
Object.entries(envNode.envs).forEach(([key, value]) => {
this.envs[key] = [value, envNode];
});
});
}
}
export class EnvNode extends DLNode {
constructor(envs, depsArr) {
super(DLNodeType.Env);
// Declare a global variable to store the environment variables
if (!('DLEnvStore' in DLStore.global)) DLStore.global.DLEnvStore = new EnvStoreClass();
this.envs = envs;
this.depsArr = depsArr;
this.updateNodes = new Set();
DLStore.global.DLEnvStore.addEnvNode(this);
}
cached(deps, name) {
if (!deps || !deps.length) return false;
if (cached(deps, this.depsArr[name])) return true;
this.depsArr[name] = deps;
return false;
}
/**
* @brief Update a specific env, and update all the comp nodes that depend on this env
* @param name - The name of the environment variable to update
* @param value - The new value of the environment variable
*/
updateEnv(name, valueFunc, deps) {
if (this.cached(deps, name)) return;
const value = valueFunc();
this.envs[name] = value;
if (DLStore.global.DLEnvStore.currentEnvNodes.includes(this)) {
DLStore.global.DLEnvStore.mergeEnvs();
}
this.updateNodes.forEach(node => {
node._$updateEnv(name, value, this);
});
}
/**
* @brief Add a node to this.updateNodes, delete the node from this.updateNodes when it unmounts
* @param node - The node to add
*/
addNode(node) {
this.updateNodes.add(node);
DLNode.addWillUnmount(node, this.updateNodes.delete.bind(this.updateNodes, node));
}
/**
* @brief Set this._$nodes, and exit the current env
* @param nodes - The nodes to set
*/
initNodes(nodes) {
this._$nodes = nodes;
DLStore.global.DLEnvStore.removeEnvNode();
}
}

View File

@ -0,0 +1,163 @@
import { DLNode } from './DLNode';
import { DLStore, cached } from './store';
function cache(el, key, deps) {
if (deps.length === 0) return false;
const cacheKey = `$${key}`;
if (cached(deps, el[cacheKey])) return true;
el[cacheKey] = deps;
return false;
}
/**
* @brief Plainly set style
* @param el
* @param value
*/
export function setStyle(el, value) {
Object.entries(value).forEach(([key, value]) => {
if (key.startsWith('--')) {
el.style.setProperty(key, value);
} else {
el.style[key] = value;
}
});
}
/**
* @brief Plainly set dataset
* @param el
* @param value
*/
export function setDataset(el, value) {
Object.assign(el.dataset, value);
}
/**
* @brief Set HTML property with checking value equality first
* @param el
* @param key
* @param value
*/
export function setHTMLProp(el, key, valueFunc, deps) {
// ---- Comparing deps, same value won't trigger
// will lead to a bug if the value is set outside of the DLNode
// e.g. setHTMLProp(el, "textContent", "value", [])
// => el.textContent = "other"
// => setHTMLProp(el, "textContent", "value", [])
// The value will be set to "other" instead of "value"
if (cache(el, key, deps)) return;
el[key] = valueFunc();
}
/**
* @brief Plainly set HTML properties
* @param el
* @param value
*/
export function setHTMLProps(el, value) {
Object.entries(value).forEach(([key, v]) => {
if (key === 'style') return setStyle(el, v);
if (key === 'dataset') return setDataset(el, v);
setHTMLProp(el, key, () => v, []);
});
}
/**
* @brief Set HTML attribute with checking value equality first
* @param el
* @param key
* @param value
*/
export function setHTMLAttr(el, key, valueFunc, deps) {
if (cache(el, key, deps)) return;
el.setAttribute(key, valueFunc());
}
/**
* @brief Plainly set HTML attributes
* @param el
* @param value
*/
export function setHTMLAttrs(el, value) {
Object.entries(value).forEach(([key, v]) => {
setHTMLAttr(el, key, () => v, []);
});
}
/**
* @brief Set memorized event, store the previous event in el[`$on${key}`], if it exists, remove it first
* @param el
* @param key
* @param value
*/
export function setEvent(el, key, value) {
const prevEvent = el[`$on${key}`];
if (prevEvent) el.removeEventListener(key, prevEvent);
el.addEventListener(key, value);
el[`$on${key}`] = value;
}
function eventHandler(e) {
const key = `$$${e.type}`;
for (const node of e.composedPath()) {
if (node[key]) node[key](e);
if (e.cancelBubble) return;
}
}
export function delegateEvent(el, key, value) {
if (el[`$$${key}`] === value) return;
el[`$$${key}`] = value;
if (!DLStore.delegatedEvents.has(key)) {
DLStore.delegatedEvents.add(key);
DLStore.document.addEventListener(key, eventHandler);
}
}
/**
* @brief Shortcut for document.createElement
* @param tag
* @returns HTMLElement
*/
export function createElement(tag) {
return DLStore.document.createElement(tag);
}
/**
* @brief Insert any DLNode into an element, set the _$nodes and append the element to the element's children
* @param el
* @param node
* @param position
*/
export function insertNode(el, node, position) {
// ---- Set _$nodes
if (!el._$nodes) el._$nodes = Array.from(el.childNodes);
el._$nodes.splice(position, 0, node);
// ---- Insert nodes' elements
const flowIdx = DLNode.getFlowIndexFromNodes(el._$nodes, node);
DLNode.appendNodesWithIndex([node], el, flowIdx);
// ---- Set parentEl
DLNode.addParentEl([node], el);
}
/**
* @brief An inclusive assign prop function that accepts any type of prop
* @param el
* @param key
* @param value
*/
export function forwardHTMLProp(el, key, valueFunc, deps) {
if (key === 'style') return setStyle(el, valueFunc());
if (key === 'dataset') return setDataset(el, valueFunc());
if (key === 'element') return;
if (key === 'prop') return setHTMLProps(el, valueFunc());
if (key === 'attr') return setHTMLAttrs(el, valueFunc());
if (key === 'innerHTML') return setHTMLProp(el, 'innerHTML', valueFunc, deps);
if (key === 'textContent') return setHTMLProp(el, 'textContent', valueFunc, deps);
if (key === 'forwardProp') return;
if (key.startsWith('on')) {
return setEvent(el, key.slice(2).toLowerCase(), valueFunc());
}
setHTMLAttr(el, key, valueFunc, deps);
}

View File

@ -0,0 +1,69 @@
import { DLNodeType } from '../DLNode';
import { FlatNode } from './FlatNode';
export class CondNode extends FlatNode {
/**
* @brief Constructor, If type, accept a function that returns a list of nodes
* @param caseFunc
*/
constructor(depNum, condFunc) {
super(DLNodeType.Cond);
this.depNum = depNum;
this.cond = -1;
this.condFunc = condFunc;
this.initUnmountStore();
this._$nodes = this.condFunc(this);
this.setUnmountFuncs();
// ---- Add to the global UnmountStore
CondNode.addWillUnmount(this, this.runWillUnmount.bind(this));
CondNode.addDidUnmount(this, this.runDidUnmount.bind(this));
}
/**
* @brief Update the nodes in the environment
*/
updateCond(key) {
// ---- Need to save prev unmount funcs because we can't put removeNodes before geneNewNodesInEnv
// The reason is that if it didn't change, we don't need to unmount or remove the nodes
const prevFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
const newNodes = this.geneNewNodesInEnv(() => this.condFunc(this));
// ---- If the new nodes are the same as the old nodes, we only need to update children
if (this.didntChange) {
[this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
this.didntChange = false;
this.updateFunc?.(this.depNum, key);
return;
}
// ---- Remove old nodes
const newFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
[this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
this._$nodes && this._$nodes.length > 0 && this.removeNodes(this._$nodes);
[this.willUnmountFuncs, this.didUnmountFuncs] = newFuncs;
if (newNodes.length === 0) {
// ---- No branch has been taken
this._$nodes = [];
return;
}
// ---- Add new nodes
const parentEl = this._$parentEl;
// ---- Faster append with nextSibling rather than flowIndex
const flowIndex = CondNode.getFlowIndexFromNodes(parentEl._$nodes, this);
const nextSibling = parentEl.childNodes[flowIndex];
CondNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
CondNode.runDidMount();
this._$nodes = newNodes;
}
/**
* @brief The update function of IfNode's childNodes is stored in the first child node
* @param changed
*/
update(changed) {
if (!(~this.depNum & changed)) return;
this.updateFunc?.(changed);
}
}

View File

@ -0,0 +1,86 @@
import { DLNodeType } from '../DLNode';
import { FlatNode } from './FlatNode';
import { DLStore, cached } from '../store';
export class ExpNode extends FlatNode {
/**
* @brief Constructor, Exp type, accept a function that returns a list of nodes
* @param nodesFunc
*/
constructor(value, deps) {
super(DLNodeType.Exp);
this.initUnmountStore();
this._$nodes = ExpNode.formatNodes(value);
this.setUnmountFuncs();
this.deps = this.parseDeps(deps);
// ---- Add to the global UnmountStore
ExpNode.addWillUnmount(this, this.runWillUnmount.bind(this));
ExpNode.addDidUnmount(this, this.runDidUnmount.bind(this));
}
parseDeps(deps) {
return deps.map(dep => {
// ---- CompNode
if (dep?.prototype?._$init) return dep.toString();
// ---- SnippetNode
if (dep?.propViewFunc) return dep.propViewFunc.toString();
return dep;
});
}
cache(deps) {
if (!deps || !deps.length) return false;
deps = this.parseDeps(deps);
if (cached(deps, this.deps)) return true;
this.deps = deps;
return false;
}
/**
* @brief Generate new nodes and replace the old nodes
*/
update(valueFunc, deps) {
if (this.cache(deps)) return;
this.removeNodes(this._$nodes);
const newNodes = this.geneNewNodesInEnv(() => ExpNode.formatNodes(valueFunc()));
if (newNodes.length === 0) {
this._$nodes = [];
return;
}
// ---- Add new nodes
const parentEl = this._$parentEl;
const flowIndex = ExpNode.getFlowIndexFromNodes(parentEl._$nodes, this);
const nextSibling = parentEl.childNodes[flowIndex];
ExpNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
ExpNode.runDidMount();
this._$nodes = newNodes;
}
/**
* @brief Format the nodes
* @param nodes
* @returns New nodes
*/
static formatNodes(nodes) {
if (!Array.isArray(nodes)) nodes = [nodes];
return (
nodes
// ---- Flatten the nodes
.flat(1)
// ---- Filter out empty nodes
.filter(node => node !== undefined && node !== null && typeof node !== 'boolean')
.map(node => {
// ---- If the node is a string, number or bigint, convert it to a text node
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
return DLStore.document.createTextNode(`${node}`);
}
// ---- If the node has PropView, call it to get the view
if ('propViewFunc' in node) return node.build();
return node;
})
// ---- Flatten the nodes again
.flat(1)
);
}
}

View File

@ -0,0 +1,33 @@
import { DLStore } from '../store';
import { MutableNode } from './MutableNode';
export class FlatNode extends MutableNode {
willUnmountFuncs = [];
didUnmountFuncs = [];
setUnmountFuncs() {
this.willUnmountFuncs = DLStore.global.WillUnmountStore.pop();
this.didUnmountFuncs = DLStore.global.DidUnmountStore.pop();
}
runWillUnmount() {
for (let i = 0; i < this.willUnmountFuncs.length; i++) this.willUnmountFuncs[i]();
}
runDidUnmount() {
for (let i = this.didUnmountFuncs.length - 1; i >= 0; i--) this.didUnmountFuncs[i]();
}
removeNodes(nodes) {
this.runWillUnmount();
super.removeNodes(nodes);
this.runDidUnmount();
}
geneNewNodesInEnv(newNodesFunc) {
this.initUnmountStore();
const nodes = super.geneNewNodesInEnv(newNodesFunc);
this.setUnmountFuncs();
return nodes;
}
}

View File

@ -0,0 +1,406 @@
import { DLNodeType } from '../DLNode';
import { DLStore } from '../store';
import { MutableNode } from './MutableNode';
export class ForNode extends MutableNode {
array;
nodeFunc;
depNum;
nodesMap = new Map();
updateArr = [];
/**
* @brief Getter for nodes
*/
get _$nodes() {
const nodes = [];
for (let idx = 0; idx < this.array.length; idx++) {
nodes.push(...this.nodesMap.get(this.keys?.[idx] ?? idx));
}
return nodes;
}
/**
* @brief Constructor, For type
* @param array
* @param nodeFunc
* @param keys
*/
constructor(array, depNum, keys, nodeFunc) {
super(DLNodeType.For);
this.array = [...array];
this.keys = keys;
this.depNum = depNum;
this.addNodeFunc(nodeFunc);
}
/**
* @brief To be called immediately after the constructor
* @param nodeFunc
*/
addNodeFunc(nodeFunc) {
this.nodeFunc = nodeFunc;
this.array.forEach((item, idx) => {
this.initUnmountStore();
const key = this.keys?.[idx] ?? idx;
const nodes = nodeFunc(item, this.updateArr, idx);
this.nodesMap.set(key, nodes);
this.setUnmountMap(key);
});
// ---- For nested ForNode, the whole strategy is just like EnvStore
// we use array of function array to create "environment", popping and pushing
ForNode.addWillUnmount(this, this.runAllWillUnmount.bind(this));
ForNode.addDidUnmount(this, this.runAllDidUnmount.bind(this));
}
/**
* @brief Update the view related to one item in the array
* @param nodes
* @param item
*/
updateItem(idx, array, changed) {
// ---- The update function of ForNode's childNodes is stored in the first child node
this.updateArr[idx]?.(changed ?? this.depNum, array[idx]);
}
updateItems(changed) {
for (let idx = 0; idx < this.array.length; idx++) {
this.updateItem(idx, this.array, changed);
}
}
/**
* @brief Non-array update function
* @param changed
*/
update(changed) {
// ---- e.g. this.depNum -> 1110 changed-> 1010
// ~this.depNum & changed -> ~1110 & 1010 -> 0000
// no update because depNum contains all the changed
// ---- e.g. this.depNum -> 1110 changed-> 1101
// ~this.depNum & changed -> ~1110 & 1101 -> 0001
// update because depNum doesn't contain all the changed
if (!(~this.depNum & changed)) return;
this.updateItems(changed);
}
/**
* @brief Array-related update function
* @param newArray
* @param newKeys
*/
updateArray(newArray, newKeys) {
if (newKeys) {
this.updateWithKey(newArray, newKeys);
return;
}
this.updateWithOutKey(newArray);
}
/**
* @brief Shortcut to generate new nodes with idx and key
*/
getNewNodes(idx, key, array, updateArr) {
this.initUnmountStore();
const nodes = this.geneNewNodesInEnv(() => this.nodeFunc(array[idx], updateArr ?? this.updateArr, idx));
this.setUnmountMap(key);
this.nodesMap.set(key, nodes);
return nodes;
}
/**
* @brief Set the unmount map by getting the last unmount map from the global store
* @param key
*/
setUnmountMap(key) {
const willUnmountMap = DLStore.global.WillUnmountStore.pop();
if (willUnmountMap && willUnmountMap.length > 0) {
if (!this.willUnmountMap) this.willUnmountMap = new Map();
this.willUnmountMap.set(key, willUnmountMap);
}
const didUnmountMap = DLStore.global.DidUnmountStore.pop();
if (didUnmountMap && didUnmountMap.length > 0) {
if (!this.didUnmountMap) this.didUnmountMap = new Map();
this.didUnmountMap.set(key, didUnmountMap);
}
}
/**
* @brief Run all the unmount functions and clear the unmount map
*/
runAllWillUnmount() {
if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
this.willUnmountMap.forEach(funcs => {
for (let i = 0; i < funcs.length; i++) funcs[i]?.();
});
this.willUnmountMap.clear();
}
/**
* @brief Run all the unmount functions and clear the unmount map
*/
runAllDidUnmount() {
if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
this.didUnmountMap.forEach(funcs => {
for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
});
this.didUnmountMap.clear();
}
/**
* @brief Run the unmount functions of the given key
* @param key
*/
runWillUnmount(key) {
if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
const funcs = this.willUnmountMap.get(key);
if (!funcs) return;
for (let i = 0; i < funcs.length; i++) funcs[i]?.();
this.willUnmountMap.delete(key);
}
/**
* @brief Run the unmount functions of the given key
*/
runDidUnmount(key) {
if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
const funcs = this.didUnmountMap.get(key);
if (!funcs) return;
for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
this.didUnmountMap.delete(key);
}
/**
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
* @param nodes
* @param key
*/
removeNodes(nodes, key) {
this.runWillUnmount(key);
super.removeNodes(nodes);
this.runDidUnmount(key);
this.nodesMap.delete(key);
}
/**
* @brief Update the nodes without keys
* @param newArray
*/
updateWithOutKey(newArray) {
const preLength = this.array.length;
const currLength = newArray.length;
if (preLength === currLength) {
// ---- If the length is the same, we only need to update the nodes
for (let idx = 0; idx < this.array.length; idx++) {
this.updateItem(idx, newArray);
}
this.array = [...newArray];
return;
}
const parentEl = this._$parentEl;
// ---- If the new array is longer, add new nodes directly
if (preLength < currLength) {
let flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
// ---- Calling parentEl.childNodes.length is time-consuming,
// so we use a length variable to store the length
const length = parentEl.childNodes.length;
for (let idx = 0; idx < currLength; idx++) {
if (idx < preLength) {
flowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(idx));
this.updateItem(idx, newArray);
continue;
}
const newNodes = this.getNewNodes(idx, idx, newArray);
ForNode.appendNodesWithIndex(newNodes, parentEl, flowIndex, length);
}
ForNode.runDidMount();
this.array = [...newArray];
return;
}
// ---- Update the nodes first
for (let idx = 0; idx < currLength; idx++) {
this.updateItem(idx, newArray);
}
// ---- If the new array is shorter, remove the extra nodes
for (let idx = currLength; idx < preLength; idx++) {
const nodes = this.nodesMap.get(idx);
this.removeNodes(nodes, idx);
}
this.updateArr.splice(currLength, preLength - currLength);
this.array = [...newArray];
}
/**
* @brief Update the nodes with keys
* @param newArray
* @param newKeys
*/
updateWithKey(newArray, newKeys) {
if (newKeys.length !== new Set(newKeys).size) {
throw new Error('DLight: Duplicate keys in for loop are not allowed');
}
const prevKeys = this.keys;
this.keys = newKeys;
if (ForNode.arrayEqual(prevKeys, this.keys)) {
// ---- If the keys are the same, we only need to update the nodes
for (let idx = 0; idx < newArray.length; idx++) {
this.updateItem(idx, newArray);
}
this.array = [...newArray];
return;
}
const parentEl = this._$parentEl;
// ---- No nodes after, delete all nodes
if (this.keys.length === 0) {
const parentNodes = parentEl._$nodes ?? [];
if (parentNodes.length === 1 && parentNodes[0] === this) {
// ---- ForNode is the only node in the parent node
// Frequently used in real life scenarios because we tend to always wrap for with a div element,
// so we optimize it here
this.runAllWillUnmount();
parentEl.innerHTML = '';
this.runAllDidUnmount();
} else {
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
const prevKey = prevKeys[prevIdx];
this.removeNodes(this.nodesMap.get(prevKey), prevKey);
}
}
this.nodesMap.clear();
this.updateArr = [];
this.array = [];
return;
}
// ---- Record how many nodes are before this ForNode with the same parentNode
const flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
// ---- No nodes before, append all nodes
if (prevKeys.length === 0) {
const nextSibling = parentEl.childNodes[flowIndex];
for (let idx = 0; idx < this.keys.length; idx++) {
const newNodes = this.getNewNodes(idx, this.keys[idx], newArray);
ForNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
}
ForNode.runDidMount();
this.array = [...newArray];
return;
}
const shuffleKeys = [];
const newUpdateArr = [];
// ---- 1. Delete the nodes that are no longer in the array
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
const prevKey = prevKeys[prevIdx];
if (this.keys.includes(prevKey)) {
shuffleKeys.push(prevKey);
newUpdateArr.push(this.updateArr[prevIdx]);
continue;
}
this.removeNodes(this.nodesMap.get(prevKey), prevKey);
}
// ---- 2. Add the nodes that are not in the array but in the new array
// ---- Calling parentEl.childNodes.length is time-consuming,
// so we use a length variable to store the length
let length = parentEl.childNodes.length;
let newFlowIndex = flowIndex;
for (let idx = 0; idx < this.keys.length; idx++) {
const key = this.keys[idx];
const prevIdx = shuffleKeys.indexOf(key);
if (prevIdx !== -1) {
// ---- These nodes are already in the parentEl,
// and we need to keep track of their flowIndex
newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
newUpdateArr[prevIdx]?.(this.depNum, newArray[idx]);
continue;
}
// ---- Insert updateArr first because in getNewNode the updateFunc will replace this null
newUpdateArr.splice(idx, 0, null);
const newNodes = this.getNewNodes(idx, key, newArray, newUpdateArr);
// ---- Add the new nodes
shuffleKeys.splice(idx, 0, key);
const count = ForNode.appendNodesWithIndex(newNodes, parentEl, newFlowIndex, length);
newFlowIndex += count;
length += count;
}
ForNode.runDidMount();
// ---- After adding and deleting, the only thing left is to reorder the nodes,
// but if the keys are the same, we don't need to reorder
if (ForNode.arrayEqual(this.keys, shuffleKeys)) {
this.array = [...newArray];
this.updateArr = newUpdateArr;
return;
}
newFlowIndex = flowIndex;
const bufferNodes = new Map();
// ---- 3. Replace the nodes in the same position using Fisher-Yates shuffle algorithm
for (let idx = 0; idx < this.keys.length; idx++) {
const key = this.keys[idx];
const prevIdx = shuffleKeys.indexOf(key);
const bufferedNode = bufferNodes.get(key);
if (bufferedNode) {
// ---- We need to add the flowIndex of the bufferedNode,
// because the bufferedNode is in the parentEl and the new position is ahead of the previous position
const bufferedFlowIndex = ForNode.getFlowIndexFromNodes(bufferedNode);
const lastEl = ForNode.toEls(bufferedNode).pop();
const nextSibling = parentEl.childNodes[newFlowIndex + bufferedFlowIndex];
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
// ---- If the node is buffered, we need to add it to the parentEl
ForNode.insertNodesBefore(bufferedNode, parentEl, nextSibling);
}
// ---- So the added length is the length of the bufferedNode
newFlowIndex += bufferedFlowIndex;
delete bufferNodes[idx];
} else if (prevIdx === idx) {
// ---- If the node is in the same position, we don't need to do anything
newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
continue;
} else {
// ---- If the node is not in the same position, we need to buffer it
// We buffer the node of the previous position, and then replace it with the node of the current position
const prevKey = shuffleKeys[idx];
bufferNodes.set(prevKey, this.nodesMap.get(prevKey));
// ---- Length would never change, and the last will always be in the same position,
// so it'll always be insertBefore instead of appendChild
const childNodes = this.nodesMap.get(key);
const lastEl = ForNode.toEls(childNodes).pop();
const nextSibling = parentEl.childNodes[newFlowIndex];
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
newFlowIndex += ForNode.insertNodesBefore(childNodes, parentEl, nextSibling);
}
}
// ---- Swap the keys
const tempKey = shuffleKeys[idx];
shuffleKeys[idx] = shuffleKeys[prevIdx];
shuffleKeys[prevIdx] = tempKey;
const tempUpdateFunc = newUpdateArr[idx];
newUpdateArr[idx] = newUpdateArr[prevIdx];
newUpdateArr[prevIdx] = tempUpdateFunc;
}
this.array = [...newArray];
this.updateArr = newUpdateArr;
}
/**
* @brief Compare two arrays
* @param arr1
* @param arr2
* @returns
*/
static arrayEqual(arr1, arr2) {
if (arr1.length !== arr2.length) return false;
return arr1.every((item, idx) => item === arr2[idx]);
}
}

View File

@ -0,0 +1,71 @@
import { DLNode } from '../DLNode';
import { DLStore } from '../store';
export class MutableNode extends DLNode {
/**
* @brief Mutable node is a node that this._$nodes can be changed, things need to pay attention:
* 1. The environment of the new nodes should be the same as the old nodes
* 2. The new nodes should be added to the parentEl
* 3. The old nodes should be removed from the parentEl
* @param type
*/
constructor(type) {
super(type);
// ---- Save the current environment nodes, must be a new reference
if (DLStore.global.DLEnvStore && DLStore.global.DLEnvStore.currentEnvNodes.length > 0) {
this.savedEnvNodes = [...DLStore.global.DLEnvStore.currentEnvNodes];
}
}
/**
* @brief Initialize the new nodes, add parentEl to all nodes
* @param nodes
*/
initNewNodes(nodes) {
// ---- Add parentEl to all nodes
DLNode.addParentEl(nodes, this._$parentEl);
}
/**
* @brief Generate new nodes in the saved environment
* @param newNodesFunc
* @returns
*/
geneNewNodesInEnv(newNodesFunc) {
if (!this.savedEnvNodes) {
// ---- No saved environment, just generate new nodes
const newNodes = newNodesFunc();
// ---- Only for IfNode's same condition return
// ---- Initialize the new nodes
this.initNewNodes(newNodes);
return newNodes;
}
// ---- Save the current environment nodes
const currentEnvNodes = DLStore.global.DLEnvStore.currentEnvNodes;
// ---- Replace the saved environment nodes
DLStore.global.DLEnvStore.replaceEnvNodes(this.savedEnvNodes);
const newNodes = newNodesFunc();
// ---- Retrieve the current environment nodes
DLStore.global.DLEnvStore.replaceEnvNodes(currentEnvNodes);
// ---- Only for IfNode's same condition return
// ---- Initialize the new nodes
this.initNewNodes(newNodes);
return newNodes;
}
initUnmountStore() {
DLStore.global.WillUnmountStore.push([]);
DLStore.global.DidUnmountStore.push([]);
}
/**
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
* @param nodes
* @param removeEl Only remove outermost element
*/
removeNodes(nodes) {
DLNode.loopShallowEls(nodes, node => {
this._$parentEl.removeChild(node);
});
}
}

View File

@ -0,0 +1,45 @@
import { DLNodeType } from '../DLNode';
import { FlatNode } from './FlatNode';
import { EnvNode } from '../EnvNode';
export class TryNode extends FlatNode {
constructor(tryFunc, catchFunc) {
super(DLNodeType.Try);
this.tryFunc = tryFunc;
const catchable = this.getCatchable(catchFunc);
this.envNode = new EnvNode({ _$catchable: catchable });
const nodes = tryFunc(this.setUpdateFunc.bind(this), catchable) ?? [];
this.envNode.initNodes(nodes);
this._$nodes = nodes;
}
update(changed) {
this.updateFunc?.(changed);
}
setUpdateFunc(updateFunc) {
this.updateFunc = updateFunc;
}
getCatchable(catchFunc) {
return callback =>
(...args) => {
try {
return callback(...args);
} catch (e) {
// ---- Run it in next tick to make sure when error occurs before
// didMount, this._$parentEl is not null
Promise.resolve().then(() => {
const nodes = this.geneNewNodesInEnv(() => catchFunc(this.setUpdateFunc.bind(this), e));
this._$nodes && this.removeNodes(this._$nodes);
const parentEl = this._$parentEl;
const flowIndex = FlatNode.getFlowIndexFromNodes(parentEl._$nodes, this);
const nextSibling = parentEl.childNodes[flowIndex];
FlatNode.appendNodesWithSibling(nodes, parentEl, nextSibling);
FlatNode.runDidMount();
this._$nodes = nodes;
});
}
};
}
}

View File

@ -0,0 +1,48 @@
import { DLNode } from './DLNode';
import { insertNode } from './HTMLNode';
export class PropView {
propViewFunc;
dlUpdateFunc = new Set();
/**
* @brief PropView constructor, accept a function that returns a list of DLNode
* @param propViewFunc - A function that when called, collects and returns an array of DLNode instances
*/
constructor(propViewFunc) {
this.propViewFunc = propViewFunc;
}
/**
* @brief Build the prop view by calling the propViewFunc and add every single instance of the returned DLNode to dlUpdateNodes
* @returns An array of DLNode instances returned by propViewFunc
*/
build() {
let update;
const addUpdate = updateFunc => {
update = updateFunc;
this.dlUpdateFunc.add(updateFunc);
};
const newNodes = this.propViewFunc(addUpdate);
if (newNodes.length === 0) return [];
if (update) {
// Remove the updateNode from dlUpdateNodes when it unmounts
DLNode.addWillUnmount(newNodes[0], this.dlUpdateFunc.delete.bind(this.dlUpdateFunc, update));
}
return newNodes;
}
/**
* @brief Update every node in dlUpdateNodes
* @param changed - A parameter indicating what changed to trigger the update
*/
update(...args) {
this.dlUpdateFunc.forEach(update => {
update(...args);
});
}
}
export function insertChildren(el, propView) {
insertNode(el, { _$nodes: propView.build(), _$dlNodeType: 7 }, 0);
}

View File

@ -0,0 +1,18 @@
import { DLNode, DLNodeType } from './DLNode';
import { cached } from './store';
export class SnippetNode extends DLNode {
constructor(depsArr) {
super(DLNodeType.Snippet);
this.depsArr = depsArr;
}
cached(deps, changed) {
if (!deps || !deps.length) return false;
const idx = Math.log2(changed);
const prevDeps = this.depsArr[idx];
if (cached(deps, prevDeps)) return true;
this.depsArr[idx] = deps;
return false;
}
}

View File

@ -0,0 +1,24 @@
import { DLStore, cached } from './store';
/**
* @brief Shorten document.createTextNode
* @param value
* @returns Text
*/
export function createTextNode(value, deps) {
const node = DLStore.document.createTextNode(value);
node.$$deps = deps;
return node;
}
/**
* @brief Update text node and check if the value is changed
* @param node
* @param value
*/
export function updateText(node, valueFunc, deps) {
if (cached(deps, node.$$deps)) return;
const value = valueFunc();
node.textContent = value;
node.$$deps = deps;
}

1
packages/inula-next/src/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export * from './types/index';

View File

@ -0,0 +1,56 @@
import { DLNode } from './DLNode';
import { insertNode } from './HTMLNode';
export * from './HTMLNode';
export * from './CompNode';
export * from './EnvNode';
export * from './TextNode';
export * from './PropView';
export * from './SnippetNode';
export * from './MutableNode/ForNode';
export * from './MutableNode/ExpNode';
export * from './MutableNode/CondNode';
export * from './MutableNode/TryNode';
import { DLStore } from './store';
export { setGlobal, setDocument } from './store';
function initStore() {
// Declare a global variable to store willUnmount functions
DLStore.global.WillUnmountStore = [];
// Declare a global variable to store didUnmount functions
DLStore.global.DidUnmountStore = [];
}
export function render(idOrEl, DL) {
let el = idOrEl;
if (typeof idOrEl === 'string') {
const elFound = DLStore.document.getElementById(idOrEl);
if (elFound) el = elFound;
else {
throw new Error(`DLight: Element with id ${idOrEl} not found`);
}
}
initStore();
el.innerHTML = '';
const dlNode = new DL();
dlNode._$init();
insertNode(el, dlNode, 0);
DLNode.runDidMount();
}
export function manual(callback, _deps) {
return callback();
}
export function escape(arg) {
return arg;
}
export const $ = escape;
export const required = null;
export function use() {
console.error(
'DLight: use() is not supported be called directly. You can only assign `use(model)` to a dlight class property. Any other expressions are not allowed.'
);
}

View File

@ -0,0 +1,42 @@
import { Store } from '@openinula/store';
// ---- Using external Store to store global and document
// Because Store is a singleton, it is safe to use it as a global variable
// If created in DLight package, different package versions will introduce
// multiple Store instances.
if (!('global' in Store)) {
if (typeof window !== 'undefined') {
Store.global = window;
} else if (typeof global !== 'undefined') {
Store.global = global;
} else {
Store.global = {};
}
}
if (!('document' in Store)) {
if (typeof document !== 'undefined') {
Store.document = document;
}
}
export const DLStore = { ...Store, delegatedEvents: new Set() };
export function setGlobal(globalObj) {
DLStore.global = globalObj;
}
export function setDocument(customDocument) {
DLStore.document = customDocument;
}
/**
* @brief Compare the deps with the previous deps
* @param deps
* @param prevDeps
* @returns
*/
export function cached(deps, prevDeps) {
if (!prevDeps || deps.length !== prevDeps.length) return false;
return deps.every((dep, i) => !(dep instanceof Object) && prevDeps[i] === dep);
}

View File

@ -0,0 +1,74 @@
import { type DLightHTMLAttributes } from './htmlTag';
// a very magical solution
// when vscode parse ts, if it is type A<T> = B<xxx<T>>, it will show the detailed type,
// but if type A<T> = B<xxx<T>> & xxx, it will only show alias (here is A)
// because I don't want to expose the detailed type, so type A<T> = B<xxx<T>> & Useless
// but if type Useless = { useless: never } will cause this type to have an additional property userless
// so just don't add key!
type Useless = { [key in '']: never };
export type DLightObject<T> = {
[K in keyof T]-?: undefined extends T[K]
? (value?: T[K]) => DLightObject<Omit<T, K>>
: (value: T[K]) => DLightObject<Omit<T, K>>;
};
interface CustomNodeProps {
willMount: (node: any) => void;
didMount: (node: any) => void;
willUnmount: (node: any) => void;
didUnmount: (node: any) => void;
didUpdate: (node: any, key: string, prevValue: any, currValue: any) => void;
ref: (node: any) => void;
elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
forwardProps: true | undefined;
}
export type ContentProp<T = object> = T & { _$idContent: true };
export type RemoveOptional<T> = {
[K in keyof T]-?: T[K];
};
type IsAny<T> = { _$isAny: true } extends T ? true : false;
export type ContentKeyName<T> = {
[K in keyof T]: IsAny<T[K]> extends true
? never
: // eslint-disable-next-line @typescript-eslint/no-unused-vars
T[K] extends ContentProp<infer _>
? K
: never;
}[keyof T];
export type CheckContent<T> = RemoveOptional<T>[ContentKeyName<RemoveOptional<T>>];
type CustomClassTag<T, O> =
ContentKeyName<RemoveOptional<O>> extends undefined
? () => DLightObject<T>
: undefined extends O[ContentKeyName<RemoveOptional<O>>]
? CheckContent<O> extends ContentProp<infer U>
? (content?: U extends unknown ? any : unknown) => DLightObject<Omit<T, ContentKeyName<RemoveOptional<O>>>>
: never
: CheckContent<O> extends ContentProp<infer U>
? (content: U extends unknown ? any : unknown) => DLightObject<Omit<T, ContentKeyName<RemoveOptional<O>>>>
: never;
type CustomSnippetTag<T> = T extends { content: infer U }
? (content: U) => DLightObject<Omit<T, 'content'>>
: T extends { content?: infer U }
? (content?: U) => DLightObject<Omit<T, 'content'>>
: () => DLightObject<T>;
type CustomTagType<T, G> = CustomClassTag<
T & CustomNodeProps & (keyof G extends never ? object : DLightHTMLAttributes<G, object, HTMLElement>),
T
> &
Useless;
export type Typed<T = object, G = object> = CustomTagType<T, G> & Useless;
export type SnippetTyped<T = object> = CustomSnippetTag<T> & Useless;
export type Pretty = any;
// ---- reverse
export type UnTyped<T> = T extends Typed<infer U> ? U : never;

View File

@ -0,0 +1,6 @@
// ---- env
import { DLightObject } from './compTag';
type AnyEnv = { _$anyEnv: true };
export const env: <T = AnyEnv>() => T extends AnyEnv ? any : DLightObject<T>;

View File

@ -0,0 +1,13 @@
interface ExpressionTag {
willMount: (node: any) => void;
didMount: (node: any) => void;
willUnmount: (node: any) => void;
didUnmount: (node: any) => void;
didUpdate: <T>(node: any, key: string, prevValue: T, currValue: T) => void;
elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
ref: (node: any) => void;
}
type ExpressionTagFunc = (nodes: any) => ExpressionTag;
export const _: ExpressionTagFunc;

View File

@ -0,0 +1,516 @@
export interface DLightGlobalEventHandlers {
/**
* Fires when the user aborts the download.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/abort_event)
*/
onAbort: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationcancel_event) */
onAnimationCancel: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event) */
onAnimationEnd: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event) */
onAnimationIteration: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event) */
onAnimationStart: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/auxclick_event) */
onAuxClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/beforeinput_event) */
onBeforeInput: ((this: GlobalEventHandlers, ev: InputEvent) => any) | null;
/**
* Fires when the object loses the input focus.
* @param ev The focus event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/blur_event)
*/
onBlur: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/cancel_event) */
onCancel: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs when playback is possible, but would require further buffering.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplay_event)
*/
onCanPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplaythrough_event) */
onCanPlayThrough: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Fires when the contents of the object or selection have changed.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)
*/
onChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Fires when the user clicks the left mouse button on the object
* @param ev The mouse event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/click_event)
*/
onClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/close_event) */
onClose: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Fires when the user clicks the right mouse button in the client area, opening the context menu.
* @param ev The mouse event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/contextmenu_event)
*/
onContextMenu: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/copy_event) */
onCopy: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLTrackElement/cuechange_event) */
onCueChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/cut_event) */
onCut: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
/**
* Fires when the user double-clicks the object.
* @param ev The mouse event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/dblclick_event)
*/
onDblClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
/**
* Fires on the source object continuously during a drag operation.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drag_event)
*/
onDrag: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
/**
* Fires on the source object when the user releases the mouse at the close of a drag operation.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragend_event)
*/
onDragEnd: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
/**
* Fires on the target element when the user drags the object to a valid drop target.
* @param ev The drag event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragenter_event)
*/
onDragEnter: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
/**
* Fires on the target object when the user moves the mouse out of a valid drop target during a drag operation.
* @param ev The drag event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragleave_event)
*/
onDragLeave: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
/**
* Fires on the target element continuously while the user drags the object over a valid drop target.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragover_event)
*/
onDragOver: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
/**
* Fires on the source object when the user starts to drag a text selection or selected object.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragstart_event)
*/
onDragStart: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drop_event) */
onDrop: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
/**
* Occurs when the duration attribute is updated.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/durationchange_event)
*/
onDurationChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs when the media element is reset to its initial state.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/emptied_event)
*/
onEmptied: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs when the end of playback is reached.
* @param ev The event
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ended_event)
*/
onEnded: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Fires when an error occurs during object loading.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/error_event)
*/
onError: OnErrorEventHandler;
/**
* Fires when the object receives focus.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/focus_event)
*/
onFocus: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/formdata_event) */
onFormData: ((this: GlobalEventHandlers, ev: FormDataEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/gotpointercapture_event) */
onGotPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event) */
onInput: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/invalid_event) */
onInvalid: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Fires when the user presses a key.
* @param ev The keyboard event
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keydown_event)
*/
onKeyDown: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
/**
* Fires when the user presses an alphanumeric key.
* @param ev The event.
* @deprecated
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keypress_event)
*/
onKeyPress: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
/**
* Fires when the user releases a key.
* @param ev The keyboard event
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keyup_event)
*/
onKeyUp: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
/**
* Fires immediately after the browser loads the object.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SVGElement/load_event)
*/
onLoad: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs when media data is loaded at the current playback position.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadeddata_event)
*/
onLoadedData: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs when the duration and dimensions of the media have been determined.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadedmetadata_event)
*/
onLoadedMetadata: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs when Internet Explorer begins looking for media data.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadstart_event)
*/
onLoadStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/lostpointercapture_event) */
onLostPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
/**
* Fires when the user clicks the object with either mouse button.
* @param ev The mouse event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousedown_event)
*/
onMouseDown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseenter_event) */
onMouseEnter: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseleave_event) */
onMouseLeave: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
/**
* Fires when the user moves the mouse over the object.
* @param ev The mouse event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousemove_event)
*/
onMouseMove: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
/**
* Fires when the user moves the mouse pointer outside the boundaries of the object.
* @param ev The mouse event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseout_event)
*/
onMouseOut: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
/**
* Fires when the user moves the mouse pointer into the object.
* @param ev The mouse event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseover_event)
*/
onMouseOver: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
/**
* Fires when the user releases a mouse button while the mouse is over the object.
* @param ev The mouse event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseup_event)
*/
onMouseUp: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/paste_event) */
onPaste: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
/**
* Occurs when playback is paused.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/pause_event)
*/
onPause: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs when the play method is requested.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/play_event)
*/
onPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs when the audio or video has started playing.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/playing_event)
*/
onPlaying: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointercancel_event) */
onPointerCancel: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerdown_event) */
onPointerDown: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerenter_event) */
onPointerEnter: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerleave_event) */
onPointerLeave: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointermove_event) */
onPointerMove: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerout_event) */
onPointerOut: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerover_event) */
onPointerOver: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerup_event) */
onPointerUp: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
/**
* Occurs to indicate progress while downloading media data.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/progress_event)
*/
onProgress: ((this: GlobalEventHandlers, ev: ProgressEvent) => any) | null;
/**
* Occurs when the playback rate is increased or decreased.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ratechange_event)
*/
onRateChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Fires when the user resets a form.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/reset_event)
*/
onReset: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLVideoElement/resize_event) */
onResize: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
/**
* Fires when the user repositions the scroll box in the scroll bar on the object.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scroll_event)
*/
onScroll: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scrollend_event) */
onScrollEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/securitypolicyviolation_event) */
onSecurityPolicyViolation: ((this: GlobalEventHandlers, ev: SecurityPolicyViolationEvent) => any) | null;
/**
* Occurs when the seek operation ends.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeked_event)
*/
onSeeked: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs when the current playback position is moved.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeking_event)
*/
onSeeking: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Fires when the current selection changes.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/select_event)
*/
onSelect: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/selectionchange_event) */
onSelectionChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/selectstart_event) */
onSelectStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLSlotElement/slotchange_event) */
onSlotChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs when the download has stopped.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/stalled_event)
*/
onStalled: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/submit_event) */
onSubmit: ((this: GlobalEventHandlers, ev: SubmitEvent) => any) | null;
/**
* Occurs if the load operation has been intentionally halted.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/suspend_event)
*/
onSuspend: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs to indicate the current playback position.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/timeupdate_event)
*/
onTimeUpdate: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDetailsElement/toggle_event) */
onToggle: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitioncancel_event) */
onTransitionCancel: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event) */
onTransitionEnd: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionrun_event) */
onTransitionRun: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionstart_event) */
onTransitionStart: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
/**
* Occurs when the volume is changed, or playback is muted or unmuted.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/volumechange_event)
*/
onVolumeChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* Occurs when playback stops because the next frame of a video resource is not available.
* @param ev The event.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/waiting_event)
*/
onWaiting: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* @deprecated This is a legacy alias of `onAnimationEnd`.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event)
*/
onWebkitAnimationEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* @deprecated This is a legacy alias of `onAnimationIteration`.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event)
*/
onWebkitAnimationIteration: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* @deprecated This is a legacy alias of `onAnimationStart`.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event)
*/
onWebkitAnimationStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/**
* @deprecated This is a legacy alias of `onTransitionEnd`.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event)
*/
onWebkitTransitionEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/wheel_event) */
onWheel: ((this: GlobalEventHandlers, ev: WheelEvent) => any) | null;
}

View File

@ -0,0 +1,33 @@
import { type Properties } from 'csstype';
// ---- Used to determine whether X and Y are equal, return A if equal, otherwise B
type IfEquals<X, Y, A, B> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;
export type OmitIndexSignature<ObjectType> = {
// eslint-disable-next-line @typescript-eslint/ban-types
[KeyType in keyof ObjectType as {} extends Record<KeyType, unknown> ? never : KeyType]: ObjectType[KeyType];
};
// ---- For each key, check whether there is readonly, if there is, return never, and then Pick out is not never
type WritableKeysOf<T> = {
[P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never>;
}[keyof T];
type RemoveReadOnly<T> = Pick<T, WritableKeysOf<T>>;
// ---- Delete all functions
type OmitFunction<T> = Omit<T, { [K in keyof T]: T[K] extends (...args: any) => any ? K : never }[keyof T]>;
type OmitFuncAndReadOnly<T> = RemoveReadOnly<OmitFunction<OmitIndexSignature<T>>>;
// ---- properties
type OmitFuncAndReadOnlyProperty<G> = Omit<OmitFuncAndReadOnly<G>, 'className' | 'htmlFor' | 'style' | 'innerText'>;
type CustomCSSProperties = {
[Key in `--${string}`]: string | number;
};
export type HTMLAttributes<T> = OmitFuncAndReadOnlyProperty<T> & {
style: Properties & CustomCSSProperties;
class: string;
for: string;
};

View File

@ -0,0 +1,236 @@
import type { DLightGlobalEventHandlers } from './event';
import type { OmitIndexSignature, HTMLAttributes } from './htmlElement';
// ---- If there is an event(start with on), remove it
export type PropertyWithEvent<G> = Omit<
G,
{
[K in keyof G]: K extends `on${string}` ? K : never;
}[keyof G]
> &
DLightGlobalEventHandlers;
interface DLightHtmlProps<El> {
ref: El | ((holder: El) => void) | undefined;
prop: Record<string, string | number | boolean>;
attr: Record<string, string>;
dataset: Record<string, string>;
forwardProps: true | undefined;
willMount: (el: El) => void;
didMount: (el: El) => void;
willUnmount: (el: El) => void;
didUnmount: (el: El) => void;
didUpdate: <T>(el: El, key: string, prevValue: T, currValue: T) => void;
}
export type DLightHTMLAttributes<T, G, El> = DLightHtmlProps<El> & HTMLAttributes<T> & G;
export type DLightHTMLAttributesFunc<T, G, El> = {
[K in keyof DLightHTMLAttributes<T, G, El>]: (
value?: DLightHTMLAttributes<T, G, El>[K]
) => Omit<DLightHTMLAttributesFunc<T, G, El>, K>;
};
export type DLightHtmlTagFunc<T = HTMLElement, G = object> = (
innerText?: string | number | ((View: never) => void)
) => DLightHTMLAttributesFunc<PropertyWithEvent<OmitIndexSignature<T>>, G, T>;
export const a: DLightHtmlTagFunc<HTMLAnchorElement>;
export const abbr: DLightHtmlTagFunc;
export const address: DLightHtmlTagFunc;
export const area: DLightHtmlTagFunc<HTMLAreaElement>;
export const article: DLightHtmlTagFunc;
export const aside: DLightHtmlTagFunc;
export const audio: DLightHtmlTagFunc<HTMLAudioElement>;
export const b: DLightHtmlTagFunc;
export const base: DLightHtmlTagFunc<HTMLBaseElement>;
export const bdi: DLightHtmlTagFunc;
export const bdo: DLightHtmlTagFunc;
export const blockquote: DLightHtmlTagFunc<HTMLQuoteElement>;
export const body: DLightHtmlTagFunc<HTMLBodyElement>;
export const br: DLightHtmlTagFunc<HTMLBRElement>;
export const button: DLightHtmlTagFunc<HTMLButtonElement>;
export const canvas: DLightHtmlTagFunc<HTMLCanvasElement>;
export const caption: DLightHtmlTagFunc<HTMLTableCaptionElement>;
export const cite: DLightHtmlTagFunc;
export const code: DLightHtmlTagFunc;
export const col: DLightHtmlTagFunc<HTMLTableColElement>;
export const colgroup: DLightHtmlTagFunc<HTMLTableColElement>;
export const data: DLightHtmlTagFunc<HTMLDataElement>;
export const datalist: DLightHtmlTagFunc<HTMLDataListElement>;
export const dd: DLightHtmlTagFunc;
export const del: DLightHtmlTagFunc<HTMLModElement>;
export const details: DLightHtmlTagFunc<HTMLDetailsElement>;
export const dfn: DLightHtmlTagFunc;
export const dialog: DLightHtmlTagFunc<HTMLDialogElement>;
export const div: DLightHtmlTagFunc<HTMLDivElement>;
export const dl: DLightHtmlTagFunc<HTMLDListElement>;
export const dt: DLightHtmlTagFunc;
export const em: DLightHtmlTagFunc;
export const embed: DLightHtmlTagFunc<HTMLEmbedElement>;
export const fieldset: DLightHtmlTagFunc<HTMLFieldSetElement>;
export const figcaption: DLightHtmlTagFunc;
export const figure: DLightHtmlTagFunc;
export const footer: DLightHtmlTagFunc;
export const form: DLightHtmlTagFunc<HTMLFormElement>;
export const h1: DLightHtmlTagFunc<HTMLHeadingElement>;
export const h2: DLightHtmlTagFunc<HTMLHeadingElement>;
export const h3: DLightHtmlTagFunc<HTMLHeadingElement>;
export const h4: DLightHtmlTagFunc<HTMLHeadingElement>;
export const h5: DLightHtmlTagFunc<HTMLHeadingElement>;
export const h6: DLightHtmlTagFunc<HTMLHeadingElement>;
export const head: DLightHtmlTagFunc<HTMLHeadElement>;
export const header: DLightHtmlTagFunc;
export const hgroup: DLightHtmlTagFunc;
export const hr: DLightHtmlTagFunc<HTMLHRElement>;
export const html: DLightHtmlTagFunc<HTMLHtmlElement>;
export const i: DLightHtmlTagFunc;
export const iframe: DLightHtmlTagFunc<HTMLIFrameElement>;
export const img: DLightHtmlTagFunc<HTMLImageElement>;
export const input: DLightHtmlTagFunc<HTMLInputElement>;
export const ins: DLightHtmlTagFunc<HTMLModElement>;
export const kbd: DLightHtmlTagFunc;
export const label: DLightHtmlTagFunc<HTMLLabelElement>;
export const legend: DLightHtmlTagFunc<HTMLLegendElement>;
export const li: DLightHtmlTagFunc<HTMLLIElement>;
export const link: DLightHtmlTagFunc<HTMLLinkElement>;
export const main: DLightHtmlTagFunc;
export const map: DLightHtmlTagFunc<HTMLMapElement>;
export const mark: DLightHtmlTagFunc;
export const menu: DLightHtmlTagFunc<HTMLMenuElement>;
export const meta: DLightHtmlTagFunc<HTMLMetaElement>;
export const meter: DLightHtmlTagFunc<HTMLMeterElement>;
export const nav: DLightHtmlTagFunc;
export const noscript: DLightHtmlTagFunc;
export const object: DLightHtmlTagFunc<HTMLObjectElement>;
export const ol: DLightHtmlTagFunc<HTMLOListElement>;
export const optgroup: DLightHtmlTagFunc<HTMLOptGroupElement>;
export const option: DLightHtmlTagFunc<HTMLOptionElement>;
export const output: DLightHtmlTagFunc<HTMLOutputElement>;
export const p: DLightHtmlTagFunc<HTMLParagraphElement>;
export const picture: DLightHtmlTagFunc<HTMLPictureElement>;
export const pre: DLightHtmlTagFunc<HTMLPreElement>;
export const progress: DLightHtmlTagFunc<HTMLProgressElement>;
export const q: DLightHtmlTagFunc<HTMLQuoteElement>;
export const rp: DLightHtmlTagFunc;
export const rt: DLightHtmlTagFunc;
export const ruby: DLightHtmlTagFunc;
export const s: DLightHtmlTagFunc;
export const samp: DLightHtmlTagFunc;
export const script: DLightHtmlTagFunc<HTMLScriptElement>;
export const section: DLightHtmlTagFunc;
export const select: DLightHtmlTagFunc<HTMLSelectElement>;
export const slot: DLightHtmlTagFunc<HTMLSlotElement>;
export const small: DLightHtmlTagFunc;
export const source: DLightHtmlTagFunc<HTMLSourceElement>;
export const span: DLightHtmlTagFunc<HTMLSpanElement>;
export const strong: DLightHtmlTagFunc;
export const style: DLightHtmlTagFunc<HTMLStyleElement>;
export const sub: DLightHtmlTagFunc;
export const summary: DLightHtmlTagFunc;
export const sup: DLightHtmlTagFunc;
export const table: DLightHtmlTagFunc<HTMLTableElement>;
export const tbody: DLightHtmlTagFunc<HTMLTableSectionElement>;
export const td: DLightHtmlTagFunc<HTMLTableCellElement>;
export const template: DLightHtmlTagFunc<HTMLTemplateElement>;
export const textarea: DLightHtmlTagFunc<HTMLTextAreaElement>;
export const tfoot: DLightHtmlTagFunc<HTMLTableSectionElement>;
export const th: DLightHtmlTagFunc<HTMLTableCellElement>;
export const thead: DLightHtmlTagFunc<HTMLTableSectionElement>;
export const time: DLightHtmlTagFunc<HTMLTimeElement>;
export const title: DLightHtmlTagFunc<HTMLTitleElement>;
export const tr: DLightHtmlTagFunc<HTMLTableRowElement>;
export const track: DLightHtmlTagFunc<HTMLTrackElement>;
export const u: DLightHtmlTagFunc;
export const ul: DLightHtmlTagFunc<HTMLUListElement>;
export const var_: DLightHtmlTagFunc;
export const video: DLightHtmlTagFunc<HTMLVideoElement>;
export const wbr: DLightHtmlTagFunc;
export const acronym: DLightHtmlTagFunc;
export const applet: DLightHtmlTagFunc<HTMLUnknownElement>;
export const basefont: DLightHtmlTagFunc;
export const bgsound: DLightHtmlTagFunc<HTMLUnknownElement>;
export const big: DLightHtmlTagFunc;
export const blink: DLightHtmlTagFunc<HTMLUnknownElement>;
export const center: DLightHtmlTagFunc;
export const dir: DLightHtmlTagFunc<HTMLDirectoryElement>;
export const font: DLightHtmlTagFunc<HTMLFontElement>;
export const frame: DLightHtmlTagFunc<HTMLFrameElement>;
export const frameset: DLightHtmlTagFunc<HTMLFrameSetElement>;
export const isindex: DLightHtmlTagFunc<HTMLUnknownElement>;
export const keygen: DLightHtmlTagFunc<HTMLUnknownElement>;
export const listing: DLightHtmlTagFunc<HTMLPreElement>;
export const marquee: DLightHtmlTagFunc<HTMLMarqueeElement>;
export const menuitem: DLightHtmlTagFunc;
export const multicol: DLightHtmlTagFunc<HTMLUnknownElement>;
export const nextid: DLightHtmlTagFunc<HTMLUnknownElement>;
export const nobr: DLightHtmlTagFunc;
export const noembed: DLightHtmlTagFunc;
export const noframes: DLightHtmlTagFunc;
export const param: DLightHtmlTagFunc<HTMLParamElement>;
export const plaintext: DLightHtmlTagFunc;
export const rb: DLightHtmlTagFunc;
export const rtc: DLightHtmlTagFunc;
export const spacer: DLightHtmlTagFunc<HTMLUnknownElement>;
export const strike: DLightHtmlTagFunc;
export const tt: DLightHtmlTagFunc;
export const xmp: DLightHtmlTagFunc<HTMLPreElement>;
export const animate: DLightHtmlTagFunc<SVGAnimateElement>;
export const animateMotion: DLightHtmlTagFunc<SVGAnimateMotionElement>;
export const animateTransform: DLightHtmlTagFunc<SVGAnimateTransformElement>;
export const circle: DLightHtmlTagFunc<SVGCircleElement>;
export const clipPath: DLightHtmlTagFunc<SVGClipPathElement>;
export const defs: DLightHtmlTagFunc<SVGDefsElement>;
export const desc: DLightHtmlTagFunc<SVGDescElement>;
export const ellipse: DLightHtmlTagFunc<SVGEllipseElement>;
export const feBlend: DLightHtmlTagFunc<SVGFEBlendElement>;
export const feColorMatrix: DLightHtmlTagFunc<SVGFEColorMatrixElement>;
export const feComponentTransfer: DLightHtmlTagFunc<SVGFEComponentTransferElement>;
export const feComposite: DLightHtmlTagFunc<SVGFECompositeElement>;
export const feConvolveMatrix: DLightHtmlTagFunc<SVGFEConvolveMatrixElement>;
export const feDiffuseLighting: DLightHtmlTagFunc<SVGFEDiffuseLightingElement>;
export const feDisplacementMap: DLightHtmlTagFunc<SVGFEDisplacementMapElement>;
export const feDistantLight: DLightHtmlTagFunc<SVGFEDistantLightElement>;
export const feDropShadow: DLightHtmlTagFunc<SVGFEDropShadowElement>;
export const feFlood: DLightHtmlTagFunc<SVGFEFloodElement>;
export const feFuncA: DLightHtmlTagFunc<SVGFEFuncAElement>;
export const feFuncB: DLightHtmlTagFunc<SVGFEFuncBElement>;
export const feFuncG: DLightHtmlTagFunc<SVGFEFuncGElement>;
export const feFuncR: DLightHtmlTagFunc<SVGFEFuncRElement>;
export const feGaussianBlur: DLightHtmlTagFunc<SVGFEGaussianBlurElement>;
export const feImage: DLightHtmlTagFunc<SVGFEImageElement>;
export const feMerge: DLightHtmlTagFunc<SVGFEMergeElement>;
export const feMergeNode: DLightHtmlTagFunc<SVGFEMergeNodeElement>;
export const feMorphology: DLightHtmlTagFunc<SVGFEMorphologyElement>;
export const feOffset: DLightHtmlTagFunc<SVGFEOffsetElement>;
export const fePointLight: DLightHtmlTagFunc<SVGFEPointLightElement>;
export const feSpecularLighting: DLightHtmlTagFunc<SVGFESpecularLightingElement>;
export const feSpotLight: DLightHtmlTagFunc<SVGFESpotLightElement>;
export const feTile: DLightHtmlTagFunc<SVGFETileElement>;
export const feTurbulence: DLightHtmlTagFunc<SVGFETurbulenceElement>;
export const filter: DLightHtmlTagFunc<SVGFilterElement>;
export const foreignObject: DLightHtmlTagFunc<SVGForeignObjectElement>;
export const g: DLightHtmlTagFunc<SVGGElement>;
export const image: DLightHtmlTagFunc<SVGImageElement>;
export const line: DLightHtmlTagFunc<SVGLineElement>;
export const linearGradient: DLightHtmlTagFunc<SVGLinearGradientElement>;
export const marker: DLightHtmlTagFunc<SVGMarkerElement>;
export const mask: DLightHtmlTagFunc<SVGMaskElement>;
export const metadata: DLightHtmlTagFunc<SVGMetadataElement>;
export const mpath: DLightHtmlTagFunc<SVGMPathElement>;
export const path: DLightHtmlTagFunc<SVGPathElement>;
export const pattern: DLightHtmlTagFunc<SVGPatternElement>;
export const polygon: DLightHtmlTagFunc<SVGPolygonElement>;
export const polyline: DLightHtmlTagFunc<SVGPolylineElement>;
export const radialGradient: DLightHtmlTagFunc<SVGRadialGradientElement>;
export const rect: DLightHtmlTagFunc<SVGRectElement>;
export const set: DLightHtmlTagFunc<SVGSetElement>;
export const stop: DLightHtmlTagFunc<SVGStopElement>;
export const svg: DLightHtmlTagFunc<SVGSVGElement>;
export const switch_: DLightHtmlTagFunc<SVGSwitchElement>;
export const symbol: DLightHtmlTagFunc<SVGSymbolElement>;
export const text: DLightHtmlTagFunc<SVGTextElement>;
export const textPath: DLightHtmlTagFunc<SVGTextPathElement>;
export const tspan: DLightHtmlTagFunc<SVGTSpanElement>;
// export const use: DLightHtmlTagFunc<SVGUseElement>
export const view: DLightHtmlTagFunc<SVGViewElement>;

View File

@ -0,0 +1,41 @@
import { type Typed } from './compTag';
import { type DLightHtmlTagFunc } from './htmlTag';
export { type Properties as CSSProperties } from 'csstype';
export const comp: <T>(tag: T) => object extends T ? any : Typed<T>;
export const tag: (tag: any) => DLightHtmlTagFunc;
export { _ } from './expressionTag';
export * from './htmlTag';
export * from './compTag';
export * from './envTag';
export * from './model';
export const Static: any;
export const Children: any;
export const Content: any;
export const Prop: any;
export const Env: any;
export const Watch: any;
export const ForwardProps: any;
export const Main: any;
export const App: any;
export const Mount: (idOrEl: string | HTMLElement) => any;
// ---- With actual value
export function render(idOrEl: string | HTMLElement, DL: any): void;
export function manual<T>(callback: () => T, _deps?: any[]): T;
export function escape<T>(arg: T): T;
export function setGlobal(globalObj: any): void;
export function setDocument(customDocument: any): void;
export const $: typeof escape;
export const View: any;
export const Snippet: any;
export const Model: any;
export const update: any;
export const required: any;
export function insertChildren<T>(parent: T, children: DLightViewProp): void;
// ---- View types
export type DLightViewComp<T = any> = Typed<T>;
export type DLightViewProp = (View: any) => void;
export type DLightViewLazy<T = any> = () => Promise<{ default: T }>;

View File

@ -0,0 +1,22 @@
import { ContentKeyName, ContentProp } from './compTag';
type RemoveDLightInternal<T, Props> = Omit<T, 'willMount' | 'didMount' | 'didUpdate' | 'willUnmount' | keyof Props>;
export type Modeling<Model, Props = object> = (props: Props) => Model;
type GetProps<T> = keyof T extends never ? never : ContentKeyName<T> extends undefined ? T : Omit<T, ContentKeyName<T>>;
type GetContent<T> =
ContentKeyName<T> extends undefined ? never : T[ContentKeyName<T>] extends ContentProp<infer U> ? U : never;
export const use: <M>(
model: M,
// @ts-expect-error Model should be a function
props?: GetProps<Parameters<M>[0]>,
// @ts-expect-error Model should be a function
content?: GetContent<Parameters<M>[0]>
// @ts-expect-error Model should be a function
) => RemoveDLightInternal<ReturnType<M>, Parameters<M>[0]>;
// @ts-expect-error Model should be a function
export type ModelType<T> = RemoveDLightInternal<ReturnType<T>, Parameters<T>[0]>;

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
}

View File

@ -14,11 +14,5 @@
*/
module.exports = {
presets: [
[
'@babel/preset-env',
{ targets: { node: 'current' }},
],
['@babel/preset-typescript'],
],
presets: [['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-typescript']],
};

View File

@ -21,16 +21,19 @@ import { babel } from '@rollup/plugin-babel';
export default {
input: './index.ts',
output: [{
file: 'dist/inulaRequest.js',
format: 'umd',
exports: 'named',
name: 'inulaRequest',
sourcemap: false,
}, {
file: 'dist/inulaRequest.esm-browser.js',
format: 'esm',
}],
output: [
{
file: 'dist/inulaRequest.js',
format: 'umd',
exports: 'named',
name: 'inulaRequest',
sourcemap: false,
},
{
file: 'dist/inulaRequest.esm-browser.js',
format: 'esm',
},
],
plugins: [
resolve(),
commonjs(),

View File

@ -1,4 +1,5 @@
{
"name": "inula-router-config",
"module": "./esm/connectRouter.js",
"main": "./cjs/connectRouter.js",
"types": "./@types/index.d.ts",

View File

@ -38,7 +38,7 @@ describe('path lexer Test', () => {
expect(tokens).toStrictEqual([{ type: 'delimiter', value: '/' }]);
});
it('don\'t start with a slash', () => {
it("don't start with a slash", () => {
const func = () => lexer('abc.com');
expect(func).toThrow(Error('Url must start with "/".'));
});

View File

@ -109,13 +109,16 @@ function genConfig(mode) {
function genJSXRuntimeConfig(mode) {
return {
input: path.resolve(libDir, 'src', 'jsx-runtime.ts'),
output: [{
file: outputResolve('jsx-runtime.js'),
format: 'cjs',
}, {
file: outputResolve('jsx-runtime.esm-browser.js'),
format: 'esm',
}],
output: [
{
file: outputResolve('jsx-runtime.js'),
format: 'cjs',
},
{
file: outputResolve('jsx-runtime.esm-browser.js'),
format: 'esm',
},
],
plugins: [...getBasicPlugins(mode)],
};
}

View File

@ -31,7 +31,11 @@ export interface IObserver {
clearByVNode: (vNode: any) => void;
}
export type StoreConfig<S extends Record<string, unknown>, A extends UserActions<S>, C extends UserComputedValues<S>> = {
export type StoreConfig<
S extends Record<string, unknown>,
A extends UserActions<S>,
C extends UserComputedValues<S>,
> = {
id?: string;
state?: S;
actions?: A;
@ -45,7 +49,11 @@ export type UserActions<S extends Record<string, unknown>> = {
[K: string]: ActionFunction<S>;
};
export type ActionFunction<S extends Record<string, unknown>> = (this: StoreObj<S, any, any>, state: S, ...args: any[]) => any;
export type ActionFunction<S extends Record<string, unknown>> = (
this: StoreObj<S, any, any>,
state: S,
...args: any[]
) => any;
export type StoreActions<S extends Record<string, unknown>, A extends UserActions<S>> = {
[K in keyof A]: Action<A[K], S>;

View File

@ -0,0 +1,101 @@
# delight-transformer
This is a experimental package to implement [API2.0](https://gitee.com/openInula/rfcs/blob/master/src/002-zouyu-API2.0.md) to [dlight](https://github.com/dlight-js/dlight) class.
## Todo-list
- [ ] function 2 class.
- [x] assignment 2 property
- [x] statement 2 watch func
- [ ] handle `props` @HQ
- [x] object destructuring
- [x] default value
- [ ] partial object destructuring
- [ ] nested object destructuring
- [ ] nested array destructuring
- [ ] alias
- [x] add `this` @HQ
- [ ] for (jsx-parser) -> playground + benchmark @YH
- [ ] lifecycle @HQ
- [ ] ref @HQ (to validate)
- [ ] env @HQ (to validate)
- [ ] Sub component
- [ ] Early Return
- [ ] custom hook -> Model @YH
- [ ] JSX
- [x] style
- [x] fragment
- [ ] ref (to validate)
- [ ] snippet
- [x] for
# 4.8 TODO
@YH
* Benchmark(result + comparison)
* Playground(@HQ publish) deploy
* PPT
* DEMO
* api2.1 compiled code
# function component syntax
- [ ] props (destructuring | partial destructuring | default value | alias)
- [ ] variable declaration -> class component property
- [ ] function declaration ( arrow function | async function )-> class method
- [ ] Statement -> watch function
- [ ] assignment
- [ ] function call
- [ ] class method call
- [ ] for loop
- [ ] while loop (do while, while, for, for in, for of)
- [ ] if statement
- [ ] switch statement
- [ ] try catch statement
- [ ] throw statement ? not support
- [ ] delete expression
- [ ] lifecycle -> LabeledStatement
- [ ] return statement -> render method(Body)
- [ ] iife
- [ ] early return
# custom hook syntax
TODO
# issues
- [ ] partial props destructuring -> support this.$props @YH
```jsx
function Input({onClick, xxx, ...props}) {
function handleClick() {
onClick()
}
return <input onClick={handleClick} {...props} />
}
```
- [ ] model class declaration should before class component declaration -> use Class polyfill
```jsx
// Code like this will cause error: `FetchModel` is not defined
@Main
@View
class MyComp {
fetchModel = use(FetchModel, { url: "https://api.example.com/data" })
Body() {}
}
@Model
class FetchModel {}
```
- [ ] custom hook early return @YH
- [ ] snippet
```jsx
const H1 = <h1></h1>;
// {H1}
const H1 = (name) => <h1 className={name}></h1>;
// {H1()} <H1/>
function H1() {
return <h1></h1>;
}
// <H1/>
```

View File

@ -0,0 +1,14 @@
# babel-preset-inula-next
## 0.0.3
### Patch Changes
- Updated dependencies
- @openinula/class-transformer@0.0.2
## 0.0.2
### Patch Changes
- 2f9d373: feat: change babel import

View File

@ -0,0 +1,54 @@
{
"name": "babel-preset-inula-next",
"version": "0.0.3",
"author": {
"name": "IanDx",
"email": "iandxssxx@gmail.com"
},
"keywords": [
"dlight.js",
"babel-preset"
],
"license": "MIT",
"files": [
"dist"
],
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"build": "tsup --sourcemap",
"test": "vitest --ui"
},
"devDependencies": {
"@types/babel__core": "^7.20.5",
"@types/node": "^20.10.5",
"tsup": "^6.7.0",
"typescript": "^5.3.2"
},
"dependencies": {
"@babel/plugin-syntax-decorators": "^7.23.3",
"@babel/core": "^7.23.3",
"@babel/plugin-syntax-jsx": "7.16.7",
"@babel/plugin-syntax-typescript": "^7.23.3",
"@openinula/reactivity-parser": "workspace:*",
"@openinula/view-generator": "workspace:*",
"@openinula/view-parser": "workspace:*",
"@openinula/class-transformer": "workspace:*",
"jsx-view-parser": "workspace:*",
"minimatch": "^9.0.3",
"vitest": "^1.4.0"
},
"tsup": {
"entry": [
"src/index.ts"
],
"format": [
"cjs",
"esm"
],
"clean": true,
"dts": true
}
}

View File

@ -0,0 +1,490 @@
export const devMode = process.env.NODE_ENV === 'development';
export const alterAttributeMap = {
class: 'className',
for: 'htmlFor',
};
export const reactivityFuncNames = [
// ---- Array
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
// ---- Set
'add',
'delete',
'clear',
// ---- Map
'set',
'delete',
'clear',
];
export const defaultHTMLTags = [
'a',
'abbr',
'address',
'area',
'article',
'aside',
'audio',
'b',
'base',
'bdi',
'bdo',
'blockquote',
'body',
'br',
'button',
'canvas',
'caption',
'cite',
'code',
'col',
'colgroup',
'data',
'datalist',
'dd',
'del',
'details',
'dfn',
'dialog',
'div',
'dl',
'dt',
'em',
'embed',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'i',
'iframe',
'img',
'input',
'ins',
'kbd',
'label',
'legend',
'li',
'link',
'main',
'map',
'mark',
'menu',
'meta',
'meter',
'nav',
'noscript',
'object',
'ol',
'optgroup',
'option',
'output',
'p',
'picture',
'pre',
'progress',
'q',
'rp',
'rt',
'ruby',
's',
'samp',
'script',
'section',
'select',
'slot',
'small',
'source',
'span',
'strong',
'style',
'sub',
'summary',
'sup',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'time',
'title',
'tr',
'track',
'u',
'ul',
'var',
'video',
'wbr',
'acronym',
'applet',
'basefont',
'bgsound',
'big',
'blink',
'center',
'dir',
'font',
'frame',
'frameset',
'isindex',
'keygen',
'listing',
'marquee',
'menuitem',
'multicol',
'nextid',
'nobr',
'noembed',
'noframes',
'param',
'plaintext',
'rb',
'rtc',
'spacer',
'strike',
'tt',
'xmp',
'animate',
'animateMotion',
'animateTransform',
'circle',
'clipPath',
'defs',
'desc',
'ellipse',
'feBlend',
'feColorMatrix',
'feComponentTransfer',
'feComposite',
'feConvolveMatrix',
'feDiffuseLighting',
'feDisplacementMap',
'feDistantLight',
'feDropShadow',
'feFlood',
'feFuncA',
'feFuncB',
'feFuncG',
'feFuncR',
'feGaussianBlur',
'feImage',
'feMerge',
'feMergeNode',
'feMorphology',
'feOffset',
'fePointLight',
'feSpecularLighting',
'feSpotLight',
'feTile',
'feTurbulence',
'filter',
'foreignObject',
'g',
'image',
'line',
'linearGradient',
'marker',
'mask',
'metadata',
'mpath',
'path',
'pattern',
'polygon',
'polyline',
'radialGradient',
'rect',
'set',
'stop',
'svg',
'switch',
'symbol',
'text',
'textPath',
'tspan',
'use',
'view',
];
export const availableDecoNames = ['Static', 'Prop', 'Env', 'Content', 'Children'];
export const dlightDefaultPackageName = '@openinula/next';
export const importMap = Object.fromEntries(
[
'createElement',
'setStyle',
'setDataset',
'setEvent',
'delegateEvent',
'setHTMLProp',
'setHTMLAttr',
'setHTMLProps',
'setHTMLAttrs',
'createTextNode',
'updateText',
'insertNode',
'ForNode',
'CondNode',
'ExpNode',
'EnvNode',
'TryNode',
'SnippetNode',
'PropView',
'render',
].map(name => [name, `$$${name}`])
);
export const importsToDelete = [
'Static',
'Children',
'Content',
'Prop',
'Env',
'Watch',
'ForwardProps',
'Main',
'App',
'Mount',
'_',
'env',
'Snippet',
...defaultHTMLTags.filter(tag => tag !== 'use'),
];
/**
* @brief HTML internal attribute map, can be accessed as js property
*/
export const defaultAttributeMap = {
// ---- Other property as attribute
textContent: ['*'],
innerHTML: ['*'],
// ---- Source: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes
accept: ['form', 'input'],
// ---- Original: accept-charset
acceptCharset: ['form'],
accesskey: ['*'],
action: ['form'],
align: ['caption', 'col', 'colgroup', 'hr', 'iframe', 'img', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr'],
allow: ['iframe'],
alt: ['area', 'img', 'input'],
async: ['script'],
autocapitalize: ['*'],
autocomplete: ['form', 'input', 'select', 'textarea'],
autofocus: ['button', 'input', 'select', 'textarea'],
autoplay: ['audio', 'video'],
background: ['body', 'table', 'td', 'th'],
// ---- Original: base
bgColor: ['body', 'col', 'colgroup', 'marquee', 'table', 'tbody', 'tfoot', 'td', 'th', 'tr'],
border: ['img', 'object', 'table'],
buffered: ['audio', 'video'],
capture: ['input'],
charset: ['meta'],
checked: ['input'],
cite: ['blockquote', 'del', 'ins', 'q'],
className: ['*'],
color: ['font', 'hr'],
cols: ['textarea'],
// ---- Original: colspan
colSpan: ['td', 'th'],
content: ['meta'],
// ---- Original: contenteditable
contentEditable: ['*'],
contextmenu: ['*'],
controls: ['audio', 'video'],
coords: ['area'],
crossOrigin: ['audio', 'img', 'link', 'script', 'video'],
csp: ['iframe'],
data: ['object'],
// ---- Original: datetime
dateTime: ['del', 'ins', 'time'],
decoding: ['img'],
default: ['track'],
defer: ['script'],
dir: ['*'],
dirname: ['input', 'textarea'],
disabled: ['button', 'fieldset', 'input', 'optgroup', 'option', 'select', 'textarea'],
download: ['a', 'area'],
draggable: ['*'],
enctype: ['form'],
// ---- Original: enterkeyhint
enterKeyHint: ['textarea', 'contenteditable'],
htmlFor: ['label', 'output'],
form: ['button', 'fieldset', 'input', 'label', 'meter', 'object', 'output', 'progress', 'select', 'textarea'],
// ---- Original: formaction
formAction: ['input', 'button'],
// ---- Original: formenctype
formEnctype: ['button', 'input'],
// ---- Original: formmethod
formMethod: ['button', 'input'],
// ---- Original: formnovalidate
formNoValidate: ['button', 'input'],
// ---- Original: formtarget
formTarget: ['button', 'input'],
headers: ['td', 'th'],
height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
hidden: ['*'],
high: ['meter'],
href: ['a', 'area', 'base', 'link'],
hreflang: ['a', 'link'],
// ---- Original: http-equiv
httpEquiv: ['meta'],
id: ['*'],
integrity: ['link', 'script'],
// ---- Original: intrinsicsize
intrinsicSize: ['img'],
// ---- Original: inputmode
inputMode: ['textarea', 'contenteditable'],
ismap: ['img'],
// ---- Original: itemprop
itemProp: ['*'],
kind: ['track'],
label: ['optgroup', 'option', 'track'],
lang: ['*'],
language: ['script'],
loading: ['img', 'iframe'],
list: ['input'],
loop: ['audio', 'marquee', 'video'],
low: ['meter'],
manifest: ['html'],
max: ['input', 'meter', 'progress'],
// ---- Original: maxlength
maxLength: ['input', 'textarea'],
// ---- Original: minlength
minLength: ['input', 'textarea'],
media: ['a', 'area', 'link', 'source', 'style'],
method: ['form'],
min: ['input', 'meter'],
multiple: ['input', 'select'],
muted: ['audio', 'video'],
name: [
'button',
'form',
'fieldset',
'iframe',
'input',
'object',
'output',
'select',
'textarea',
'map',
'meta',
'param',
],
// ---- Original: novalidate
noValidate: ['form'],
open: ['details', 'dialog'],
optimum: ['meter'],
pattern: ['input'],
ping: ['a', 'area'],
placeholder: ['input', 'textarea'],
// ---- Original: playsinline
playsInline: ['video'],
poster: ['video'],
preload: ['audio', 'video'],
readonly: ['input', 'textarea'],
// ---- Original: referrerpolicy
referrerPolicy: ['a', 'area', 'iframe', 'img', 'link', 'script'],
rel: ['a', 'area', 'link'],
required: ['input', 'select', 'textarea'],
reversed: ['ol'],
role: ['*'],
rows: ['textarea'],
// ---- Original: rowspan
rowSpan: ['td', 'th'],
sandbox: ['iframe'],
scope: ['th'],
scoped: ['style'],
selected: ['option'],
shape: ['a', 'area'],
size: ['input', 'select'],
sizes: ['link', 'img', 'source'],
slot: ['*'],
span: ['col', 'colgroup'],
spellcheck: ['*'],
src: ['audio', 'embed', 'iframe', 'img', 'input', 'script', 'source', 'track', 'video'],
srcdoc: ['iframe'],
srclang: ['track'],
srcset: ['img', 'source'],
start: ['ol'],
step: ['input'],
style: ['*'],
summary: ['table'],
// ---- Original: tabindex
tabIndex: ['*'],
target: ['a', 'area', 'base', 'form'],
title: ['*'],
translate: ['*'],
type: ['button', 'input', 'embed', 'object', 'ol', 'script', 'source', 'style', 'menu', 'link'],
usemap: ['img', 'input', 'object'],
value: ['button', 'data', 'input', 'li', 'meter', 'option', 'progress', 'param', 'text' /** extra for TextNode */],
width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
wrap: ['textarea'],
// --- ARIA attributes
// Source: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
ariaAutocomplete: ['*'],
ariaChecked: ['*'],
ariaDisabled: ['*'],
ariaErrorMessage: ['*'],
ariaExpanded: ['*'],
ariaHasPopup: ['*'],
ariaHidden: ['*'],
ariaInvalid: ['*'],
ariaLabel: ['*'],
ariaLevel: ['*'],
ariaModal: ['*'],
ariaMultiline: ['*'],
ariaMultiSelectable: ['*'],
ariaOrientation: ['*'],
ariaPlaceholder: ['*'],
ariaPressed: ['*'],
ariaReadonly: ['*'],
ariaRequired: ['*'],
ariaSelected: ['*'],
ariaSort: ['*'],
ariaValuemax: ['*'],
ariaValuemin: ['*'],
ariaValueNow: ['*'],
ariaValueText: ['*'],
ariaBusy: ['*'],
ariaLive: ['*'],
ariaRelevant: ['*'],
ariaAtomic: ['*'],
ariaDropEffect: ['*'],
ariaGrabbed: ['*'],
ariaActiveDescendant: ['*'],
ariaColCount: ['*'],
ariaColIndex: ['*'],
ariaColSpan: ['*'],
ariaControls: ['*'],
ariaDescribedBy: ['*'],
ariaDescription: ['*'],
ariaDetails: ['*'],
ariaFlowTo: ['*'],
ariaLabelledBy: ['*'],
ariaOwns: ['*'],
ariaPosInset: ['*'],
ariaRowCount: ['*'],
ariaRowIndex: ['*'],
ariaRowSpan: ['*'],
ariaSetSize: ['*'],
};

View File

@ -0,0 +1,4 @@
declare module '@babel/plugin-syntax-do-expressions';
declare module '@babel/plugin-syntax-decorators';
declare module '@babel/plugin-syntax-jsx';
declare module '@babel/plugin-syntax-typescript';

View File

@ -0,0 +1,21 @@
import syntaxDecorators from '@babel/plugin-syntax-decorators';
import syntaxJSX from '@babel/plugin-syntax-jsx';
import syntaxTypescript from '@babel/plugin-syntax-typescript';
import dlight from './plugin';
import { type DLightOption } from './types';
import { type ConfigAPI, type TransformOptions } from '@babel/core';
import { plugin as fn2Class } from '@openinula/class-transformer';
export default function (_: ConfigAPI, options: DLightOption): TransformOptions {
return {
plugins: [
syntaxJSX.default ?? syntaxJSX,
[syntaxTypescript.default ?? syntaxTypescript, { isTSX: true }],
[syntaxDecorators.default ?? syntaxDecorators, { legacy: true }],
fn2Class,
[dlight, options],
],
};
}
export { type DLightOption };

View File

@ -0,0 +1,43 @@
import type babel from '@babel/core';
import { type PluginObj } from '@babel/core';
import { PluginProviderClass } from './pluginProvider';
import { type DLightOption } from './types';
import { defaultAttributeMap } from './const';
export default function (api: typeof babel, options: DLightOption): PluginObj {
const { types } = api;
const {
files = '**/*.{js,ts,jsx,tsx}',
excludeFiles = '**/{dist,node_modules,lib}/*',
enableDevTools = false,
htmlTags = defaultHtmlTags => defaultHtmlTags,
attributeMap = defaultAttributeMap,
} = options;
const pluginProvider = new PluginProviderClass(
api,
types,
Array.isArray(files) ? files : [files],
Array.isArray(excludeFiles) ? excludeFiles : [excludeFiles],
enableDevTools,
htmlTags,
attributeMap
);
return {
visitor: {
Program: {
enter(path, { filename }) {
return pluginProvider.programEnterVisitor(path, filename);
},
exit: pluginProvider.programExitVisitor.bind(pluginProvider),
},
ClassDeclaration: {
enter: pluginProvider.classEnter.bind(pluginProvider),
exit: pluginProvider.classExit.bind(pluginProvider),
},
ClassMethod: pluginProvider.classMethodVisitor.bind(pluginProvider),
ClassProperty: pluginProvider.classPropertyVisitor.bind(pluginProvider),
},
};
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
import { type types as t } from '@babel/core';
export type HTMLTags = string[] | ((defaultHtmlTags: string[]) => string[]);
export interface DLightOption {
/**
* Files that will be included
* @default ** /*.{js,jsx,ts,tsx}
*/
files?: string | string[];
/**
* Files that will be excludes
* @default ** /{dist,node_modules,lib}/*.{js,ts}
*/
excludeFiles?: string | string[];
/**
* Enable devtools
* @default false
*/
enableDevTools?: boolean;
/**
* Custom HTML tags.
* Accepts 2 types:
* 1. string[], e.g. ["div", "span"]
* if contains "*", then all default tags will be included
* 2. (defaultHtmlTags: string[]) => string[]
* @default defaultHtmlTags => defaultHtmlTags
*/
htmlTags?: HTMLTags;
/**
* Allowed HTML tags from attributes
* e.g. { alt: ["area", "img", "input"] }
*/
attributeMap?: Record<string, string[]>;
}
export type PropertyContainer = Record<
string,
{
node: t.ClassProperty | t.ClassMethod;
deps: string[];
isStatic?: boolean;
isContent?: boolean;
isChildren?: boolean | number;
isModel?: boolean;
isWatcher?: boolean;
isPropOrEnv?: 'Prop' | 'Env';
depsNode?: t.ArrayExpression;
}
>;
export type IdentifierToDepNode = t.SpreadElement | t.Expression;
export type SnippetPropSubDepMap = Record<string, Record<string, string[]>>;

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { describe, expect, it } from 'vitest';
import { transform } from './presets';
describe('condition', () => {
it('should transform jsx', () => {
expect(
transform(`
function App() {
return <div>
<if cond={count > 1}>{count} is bigger than is 1</if>
<else>{count} is smaller than 1</else>
</div>;
}
`)
).toMatchInlineSnapshot();
});
});

View File

@ -0,0 +1,233 @@
import { describe, expect, it } from 'vitest';
import { transform } from './presets';
describe('fn2Class', () => {
it('should transform jsx', () => {
expect(
transform(`
@View
class A {
Body() {
return <div></div>
}
}`)
).toMatchInlineSnapshot(`
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
class A extends View {
Body() {
let $node0;
$node0 = $$createElement("div");
return [$node0];
}
}"
`);
});
it('should transform jsx with reactive', () => {
expect(
transform(`
@Main
@View
class A {
count = 1
Body() {
return <div onClick={() => this.count++}>{this.count}</div>
}
}`)
).toMatchInlineSnapshot(`
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
class A extends View {
count = 1;
$$count = 1;
Body() {
let $node0, $node1;
this._$update = $changed => {
if ($changed & 1) {
$node1 && $node1.update(() => this.count, [this.count]);
}
};
$node0 = $$createElement("div");
$$delegateEvent($node0, "click", () => this._$ud(this.count++, "count"));
$node1 = new $$ExpNode(this.count, [this.count]);
$$insertNode($node0, $node1, 0);
$node0._$nodes = [$node1];
return [$node0];
}
}
$$render("main", A);"
`);
});
it('should transform fragment', () => {
expect(
transform(`
@View
class A {
Body() {
return <>
<div></div>
</>
}
}`)
).toMatchInlineSnapshot(`
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
class A extends View {
Body() {
let $node0;
$node0 = $$createElement("div");
return [$node0];
}
}"
`);
});
it('should transform function component', () => {
expect(
transform(`
function MyApp() {
let count = 0;
return <div onClick={() => count++}>{count}</div>
}`)
).toMatchInlineSnapshot(`
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
class MyApp extends View {
count = 0;
$$count = 1;
Body() {
let $node0, $node1;
this._$update = $changed => {
if ($changed & 1) {
$node1 && $node1.update(() => this.count, [this.count]);
}
};
$node0 = $$createElement("div");
$$delegateEvent($node0, "click", () => this._$ud(this.count++, "count"));
$node1 = new $$ExpNode(this.count, [this.count]);
$$insertNode($node0, $node1, 0);
$node0._$nodes = [$node1];
return [$node0];
}
}"
`);
});
it('should transform function component reactively', () => {
expect(
transform(`
function MyComp() {
let count = 0
return <>
<h1>Hello dlight fn, {count}</h1>
<button onClick={() => count +=1}>Add</button>
<Button />
</>
}`)
).toMatchInlineSnapshot(`
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
class MyComp extends View {
count = 0;
$$count = 1;
Body() {
let $node0, $node1, $node2, $node3, $node4;
this._$update = $changed => {
if ($changed & 1) {
$node2 && $node2.update(() => this.count, [this.count]);
}
};
$node0 = $$createElement("h1");
$node1 = $$createTextNode("Hello dlight fn, ", []);
$$insertNode($node0, $node1, 0);
$node2 = new $$ExpNode(this.count, [this.count]);
$$insertNode($node0, $node2, 1);
$node0._$nodes = [$node1, $node2];
$node3 = $$createElement("button");
$$delegateEvent($node3, "click", () => this._$ud(this.count += 1, "count"));
$node3.textContent = "Add";
$node4 = new Button();
$node4._$init(null, null, null, null);
return [$node0, $node3, $node4];
}
}"
`);
});
it('should transform children props', () => {
expect(
transform(`
function App({ children}) {
return <h1>{children}</h1>
}
`)
).toMatchInlineSnapshot(`
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
class App extends View {
get children() {
return this._$children;
}
Body() {
let $node0, $node1;
$node0 = $$createElement("h1");
$node1 = new $$ExpNode(this.children, []);
$$insertNode($node0, $node1, 0);
$node0._$nodes = [$node1];
return [$node0];
}
}"
`);
});
it('should transform component composition', () => {
expect(
transform(`
function ArrayModification({name}) {
let arr = 1
return <section>
<div>{arr}</div>
</section>
}
function MyComp() {
return <>
<ArrayModification name="1" />
</>
}
`)
).toMatchInlineSnapshot(`
"import { createElement as $$createElement, setStyle as $$setStyle, setDataset as $$setDataset, setEvent as $$setEvent, delegateEvent as $$delegateEvent, setHTMLProp as $$setHTMLProp, setHTMLAttr as $$setHTMLAttr, setHTMLProps as $$setHTMLProps, setHTMLAttrs as $$setHTMLAttrs, createTextNode as $$createTextNode, updateText as $$updateText, insertNode as $$insertNode, ForNode as $$ForNode, CondNode as $$CondNode, ExpNode as $$ExpNode, EnvNode as $$EnvNode, TryNode as $$TryNode, SnippetNode as $$SnippetNode, PropView as $$PropView, render as $$render } from "@dlightjs/dlight";
class ArrayModification extends View {
$p$name;
name;
arr = 1;
$$arr = 2;
Body() {
let $node0, $node1, $node2;
this._$update = $changed => {
if ($changed & 2) {
$node2 && $node2.update(() => this.arr, [this.arr]);
}
};
$node0 = ArrayModification.$t0.cloneNode(true);
$node1 = $node0.firstChild;
$node2 = new $$ExpNode(this.arr, [this.arr]);
$$insertNode($node1, $node2, 0);
return [$node0];
}
static $t0 = (() => {
let $node0, $node1;
$node0 = $$createElement("section");
$node1 = $$createElement("div");
$node0.appendChild($node1);
return $node0;
})();
}
class MyComp extends View {
Body() {
let $node0;
$node0 = new ArrayModification();
$node0._$init([["name", "1", []]], null, null, null);
return [$node0];
}
}"
`);
});
});

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import plugin from '../dist';
import { transform as transformWithBabel } from '@babel/core';
export function transform(code: string) {
return transformWithBabel(code, {
presets: [plugin],
filename: 'test.tsx',
})?.code;
}

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
}

View File

@ -0,0 +1,7 @@
# @openinula/class-transformer
## 0.0.2
### Patch Changes
- feat: add lifecycles and watch

View File

@ -0,0 +1,43 @@
{
"name": "@openinula/class-transformer",
"version": "0.0.2",
"description": "Inula view generator",
"keywords": [
"inula"
],
"files": [
"dist"
],
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"build": "tsup --sourcemap",
"test": "vitest --ui"
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/generator": "^7.23.6",
"@babel/traverse": "^7.24.1",
"@babel/plugin-syntax-jsx": "7.16.7",
"@types/babel__core": "^7.20.5",
"@types/babel__generator": "^7.6.8",
"@types/babel__traverse": "^7.6.8",
"@vitest/ui": "^0.34.5",
"tsup": "^6.7.0",
"typescript": "^5.3.2",
"vitest": "^0.34.5"
},
"tsup": {
"entry": [
"src/index.ts"
],
"format": [
"cjs",
"esm"
],
"clean": true,
"dts": true
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { Option } from './types';
import type { ConfigAPI, TransformOptions } from '@babel/core';
import transformer from './plugin';
export default function (_: ConfigAPI, options: Option): TransformOptions {
return {
plugins: [
['@babel/plugin-syntax-jsx'],
['@babel/plugin-syntax-typescript', { isTSX: true }],
[transformer, options],
],
};
}
export const plugin = transformer;
export type { Option };

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { PluginObj } from '@babel/core';
import { Option } from './types';
import * as babel from '@babel/core';
import { PluginProvider } from './pluginProvider';
import { ThisPatcher } from './thisPatcher';
export default function (api: typeof babel, options: Option): PluginObj {
const pluginProvider = new PluginProvider(api, options);
const thisPatcher = new ThisPatcher(api);
return {
name: 'zouyu-2',
visitor: {
FunctionDeclaration(path) {
pluginProvider.functionDeclarationVisitor(path);
thisPatcher.patch(path);
},
},
};
}

View File

@ -0,0 +1,413 @@
import { type types as t, NodePath } from '@babel/core';
import * as babel from '@babel/core';
import { Option } from './types';
import type { Scope } from '@babel/traverse';
const DECORATOR_PROPS = 'Prop';
const DECORATOR_CHILDREN = 'Children';
const DECORATOR_WATCH = 'Watch';
function replaceFnWithClass(path: NodePath<t.FunctionDeclaration>, classTransformer: ClassComponentTransformer) {
const originalName = path.node.id.name;
const tempName = path.node.id.name + 'Temp';
const classComp = classTransformer.genClassComponent(tempName);
path.replaceWith(classComp);
path.scope.rename(tempName, originalName);
}
export class PluginProvider {
// ---- Plugin Level ----
private readonly babelApi: typeof babel;
private readonly t: typeof t;
private programNode: t.Program | undefined;
constructor(babelApi: typeof babel, options: Option) {
this.babelApi = babelApi;
this.t = babelApi.types;
}
functionDeclarationVisitor(path: NodePath<t.FunctionDeclaration>): void {
// find Component function by:
// 1. has JSXElement as return value
// 2. name is capitalized
if (path.node.id?.name[0] !== path.node.id?.name[0].toUpperCase()) return;
const returnStatement = path.node.body.body.find(n => this.t.isReturnStatement(n)) as t.ReturnStatement;
if (!returnStatement) return;
if (!(this.t.isJSXElement(returnStatement.argument) || this.t.isJSXFragment(returnStatement.argument))) return;
const classTransformer = new ClassComponentTransformer(this.babelApi, path);
// transform the parameters to props
const params = path.node.params;
const props = params[0];
classTransformer.transformProps(props);
// iterate the function body orderly
const body = path.node.body.body;
body.forEach((node, idx) => {
if (this.t.isVariableDeclaration(node)) {
classTransformer.transformStateDeclaration(node);
return;
}
// handle method
if (this.t.isFunctionDeclaration(node)) {
classTransformer.transformMethods(node);
return;
}
// ---- handle lifecycles
const lifecycles = ['willMount', 'didMount', 'willUnmount', 'didUnmount'];
if (this.t.isLabeledStatement(node) && lifecycles.includes(node.label.name)) {
// transform the lifecycle statement to lifecycle method
classTransformer.transformLifeCycle(node);
return;
}
// handle watch
if (this.t.isLabeledStatement(node) && node.label.name === 'watch') {
// transform the watch statement to watch method
classTransformer.transformWatch(node);
return;
}
// handle return statement
if (this.t.isReturnStatement(node)) {
// handle early return
if (idx !== body.length - 1) {
// transform the return statement to render method
// TODO: handle early return
throw new Error('Early return is not supported yet.');
}
// transform the return statement to render method
classTransformer.transformRenderMethod(node);
return;
}
});
// replace the function declaration with class declaration
replaceFnWithClass(path, classTransformer);
}
}
type ToWatchNode =
| t.ExpressionStatement
| t.ForStatement
| t.WhileStatement
| t.IfStatement
| t.SwitchStatement
| t.TryStatement;
class ClassComponentTransformer {
properties: (t.ClassProperty | t.ClassMethod)[] = [];
// The expression to bind the nested destructuring props with prop
nestedDestructuringBindings: t.Expression[] = [];
private readonly babelApi: typeof babel;
private readonly t: typeof t;
private readonly functionScope: Scope;
valueWrapper(node) {
return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)]));
}
addProperty(prop: t.ClassProperty | t.ClassMethod, name?: string) {
this.properties.push(prop);
}
constructor(babelApi: typeof babel, fnNode: NodePath<t.FunctionDeclaration>) {
this.babelApi = babelApi;
this.t = babelApi.types;
// get the function body scope
this.functionScope = fnNode.scope;
}
// transform function component to class component extends View
genClassComponent(name: string) {
// generate ctor and push this.initExpressions to ctor
let nestedDestructuringBindingsMethod: t.ClassMethod | null = null;
if (this.nestedDestructuringBindings.length) {
nestedDestructuringBindingsMethod = this.t.classMethod(
'method',
this.t.identifier('$$bindNestDestructuring'),
[],
this.t.blockStatement([...this.nestedDestructuringBindings.map(exp => this.t.expressionStatement(exp))])
);
nestedDestructuringBindingsMethod.decorators = [this.t.decorator(this.t.identifier(DECORATOR_WATCH))];
}
return this.t.classDeclaration(
this.t.identifier(name),
this.t.identifier('View'),
this.t.classBody(
nestedDestructuringBindingsMethod ? [...this.properties, nestedDestructuringBindingsMethod] : this.properties
),
[]
);
}
/**
* Transform state declaration to class property
* if the variable is declared with `let` or `const`, it should be transformed to class property
* @param node
*/
transformStateDeclaration(node: t.VariableDeclaration) {
// iterate the declarations
node.declarations.forEach(declaration => {
const id = declaration.id;
// handle destructuring
if (this.t.isObjectPattern(id)) {
return this.transformPropsDestructuring(id);
} else if (this.t.isArrayPattern(id)) {
// TODO: handle array destructuring
} else if (this.t.isIdentifier(id)) {
// clone the id
const cloneId = this.t.cloneNode(id);
this.addProperty(this.t.classProperty(cloneId, declaration.init), id.name);
}
});
}
/**
* Transform render method to Body method
* The Body method should return the original return statement
* @param node
*/
transformRenderMethod(node: t.ReturnStatement) {
const body = this.t.classMethod(
'method',
this.t.identifier('Body'),
[],
this.t.blockStatement([node]),
false,
false
);
this.addProperty(body, 'Body');
}
transformLifeCycle(node: t.LabeledStatement) {
// transform the lifecycle statement to lifecycle method
const methodName = node.label.name;
const method = this.t.classMethod(
'method',
this.t.identifier(methodName),
[],
this.t.blockStatement(node.body.body),
false,
false
);
this.addProperty(method, methodName);
}
transformComputed() {}
transformMethods(node: t.FunctionDeclaration) {
// transform the function declaration to class method
const methodName = node.id?.name;
if (!methodName) return;
const method = this.t.classMethod(
'method',
this.t.identifier(methodName),
node.params,
node.body,
node.generator,
node.async
);
this.addProperty(method, methodName);
}
transformProps(param: t.Identifier | t.RestElement | t.Pattern) {
if (!param) return;
// handle destructuring
if (this.isObjDestructuring(param)) {
this.transformPropsDestructuring(param);
return;
}
if (this.t.isIdentifier(param)) {
// TODO: handle props identifier
return;
}
throw new Error('Unsupported props type, please use object destructuring or identifier.');
}
/**
* transform node to watch label to watch decorator
* e.g.
*
* watch: console.log(state)
* // transform into
* @Watch
* _watch() {
* console.log(state)
* }
*/
transformWatch(node: t.LabeledStatement) {
const id = this.functionScope.generateUidIdentifier(DECORATOR_WATCH.toLowerCase());
const method = this.t.classMethod('method', id, [], this.t.blockStatement([node.body]), false, false);
method.decorators = [this.t.decorator(this.t.identifier(DECORATOR_WATCH))];
this.addProperty(method);
}
private isObjDestructuring(param: t.Identifier | t.RestElement | t.Pattern): param is t.ObjectPattern {
return this.t.isObjectPattern(param);
}
/**
* how to handle default value
* ```js
* // 1. No alias
* function({name = 'defaultName'}) {}
* class A extends View {
* @Prop name = 'defaultName';
*
* // 2. Alias
* function({name: aliasName = 'defaultName'}) {}
* class A extends View {
* @Prop name = 'defaultName';
* aliasName
* @Watch
* bindAliasName() {
* this.aliasName = this.name;
* }
* }
*
* // 3. Children with default value and alias
* function({children: aliasName = 'defaultName'}) {}
* class A extends View {
* @Children aliasName = 'defaultName';
* }
* ```
*/
private transformPropsDestructuring(param: t.ObjectPattern) {
const propNames: t.Identifier[] = [];
param.properties.forEach(prop => {
if (this.t.isObjectProperty(prop)) {
let key = prop.key;
let defaultVal: t.Expression;
if (this.t.isIdentifier(key)) {
let alias: t.Identifier | null = null;
if (this.t.isAssignmentPattern(prop.value)) {
const propName = prop.value.left;
defaultVal = prop.value.right;
if (this.t.isIdentifier(propName)) {
// handle alias
if (propName.name !== key.name) {
alias = propName;
}
} else {
throw Error(`Unsupported assignment type in object destructuring: ${propName.type}`);
}
} else if (this.t.isIdentifier(prop.value)) {
// handle alias
if (key.name !== prop.value.name) {
alias = prop.value;
}
} else if (this.t.isObjectPattern(prop.value)) {
// TODO: handle nested destructuring
this.transformPropsDestructuring(prop.value);
}
const isChildren = key.name === 'children';
if (alias) {
if (isChildren) {
key = alias;
} else {
this.addClassPropertyForPropAlias(alias, key);
}
}
this.addClassProperty(key, isChildren ? DECORATOR_CHILDREN : DECORATOR_PROPS, defaultVal);
propNames.push(key);
return;
}
// handle default value
if (this.t.isAssignmentPattern(prop.value)) {
const defaultValue = prop.value.right;
const propName = prop.value.left;
//handle alias
if (this.t.isIdentifier(propName) && propName.name !== prop.key.name) {
this.addClassProperty(propName, null, undefined);
}
if (this.t.isIdentifier(propName)) {
this.addClassProperty(propName, DECORATOR_PROPS, defaultValue);
propNames.push(propName);
}
// TODO: handle nested destructuring
return;
}
throw new Error('Unsupported props destructuring, please use simple object destructuring.');
} else {
// TODO: handle rest element
}
});
return propNames;
}
private addClassPropertyForPropAlias(propName: t.Identifier, key: t.Identifier) {
// handle alias, like class A { foo: bar = 'default' }
this.addClassProperty(propName, null, undefined);
// push alias assignment in Watch , like this.bar = this.foo
this.nestedDestructuringBindings.push(
this.t.assignmentExpression('=', this.t.identifier(propName.name), this.t.identifier(key.name))
);
}
// add prop to class, like @prop name = '';
private addClassProperty(key: t.Identifier, decorator: string | null, defaultValue?: t.Expression) {
// clone the key to avoid reference issue
const id = this.t.cloneNode(key);
this.addProperty(
this.t.classProperty(
id,
defaultValue ?? undefined,
undefined,
// use prop decorator
decorator ? [this.t.decorator(this.t.identifier(decorator))] : undefined,
undefined,
false
),
key.name
);
}
/**
* Check if the node should be transformed to watch method, including:
* 1. call expression.
* 2. for loop
* 3. while loop
* 4. if statement
* 5. switch statement
* 6. assignment expression
* 7. try statement
* 8. ++/-- expression
* @param node
*/
shouldTransformWatch(node: t.Node): node is ToWatchNode {
if (this.t.isExpressionStatement(node)) {
if (this.t.isCallExpression(node.expression)) {
return true;
}
if (this.t.isAssignmentExpression(node.expression)) {
return true;
}
if (this.t.isUpdateExpression(node.expression)) {
return true;
}
}
if (this.t.isForStatement(node)) {
return true;
}
if (this.t.isWhileStatement(node)) {
return true;
}
if (this.t.isIfStatement(node)) {
return true;
}
if (this.t.isSwitchStatement(node)) {
return true;
}
if (this.t.isTryStatement(node)) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,153 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { describe, expect, it } from 'vitest';
import { transform } from './transform';
describe('component-composition', () => {
describe('props destructuring', () => {
it('should support default values', () => {
//language=JSX
expect(
transform(`
function UserProfile({
name = '',
age = null,
favouriteColors = [],
isAvailable = false,
}) {
return (
<>
<p>My name is {name}!</p>
<p>My age is {age}!</p>
<p>My favourite colors are {favouriteColors.join(', ')}!</p>
<p>I am {isAvailable ? 'available' : 'not available'}</p>
</>
);
}`),
`
class UserProfile {
@Prop name = ''
@Prop age = null
@Prop favouriteColors = []
@Prop isAvailable = false
Body() {
p(\`My name is \${this.name}!\`)
p(\`My age is \${this.age}!\`)
p(\`My favourite colors are \${this.favouriteColors.join(', ')}!\`)
p(\`I am \${this.isAvailable ? 'available' : 'not available'}\`)
}
}
`
);
});
it('should support nested destruing', () => {
//language=JSX
expect(
transform(`
function UserProfile({
name = '',
age = null,
favouriteColors : [{r,g,b}, color2],
isAvailable = false,
}) {
return (
<>
<p>My name is {name}!</p >
<p>My age is {age}!</p >
<p>My favourite colors are {favouriteColors.join(', ')}!</p >
<p>I am {isAvailable ? 'available' : 'not available'}</p >
</>
);
}`),
`
class UserProfile {
@Prop name = '';
@Prop age = null;
@Prop favouriteColors = [];
@Prop isAvailable = false;
color1;
color2;
r;
g;
b;
xx = (() => {
const [{r, g, b},color2] = this.favouriteColors;
this.r = r
this.g = g
this.b = b
this.color2 = color2
});
Body() {
p(\`My name is \${this.name}!\`);
p(\`My age is \${this.age}!\`);
p(\`My favourite colors are \${this.favouriteColors.join(', ')}!\`);
p(\`I am \${this.isAvailable ? 'available' : 'not available'}\`);
}
}
`
);
});
it('should support children prop', () => {
//language=JSX
expect(
transform(`
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}`),
`
class Card {
@Children children
Body() {
div(\`card\`, this.children)
}
}
`
);
});
});
it('should support children prop with alias', () => {
//language=JSX
expect(
transform(`
function Card({ children: content }) {
return (
<div className="card">
{children}
</div>
);
}`),
`
class Card {
@Children content
Body() {
div(\`card\`, this.children)
}
}
`
);
});
});

View File

@ -0,0 +1,19 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { test, it, expect } from 'vitest';
import { transform } from './transform';
test('conditional', () => {});

View File

@ -0,0 +1,293 @@
import { it, describe, expect } from 'vitest';
import { transform } from './transform';
describe('fn2Class', () => {
it('should transform state assignment', () => {
expect(
//language=JSX
transform(`
export default function Name() {
let name = 'John';
return <h1>{name}</h1>;
}
`)
).toMatchInlineSnapshot(`
"class Name extends View {
name = 'John';
Body() {
return <h1>{this.name}</h1>;
}
}
export { Name as default };"
`);
});
it('should transform state modification ', () => {
expect(
transform(`
function MyApp() {
let count = 0;
return <div onClick={() => count++}>{count}</div>
}
`)
).toMatchInlineSnapshot(`
"class MyApp extends View {
count = 0;
Body() {
return <div onClick={() => this.count++}>{this.count}</div>;
}
}"
`);
});
it('should not transform variable out of scope', () => {
expect(
//language=JSX
transform(`
const name = "John";
export default function Name() {
return <h1>{name}</h1>;
}
`)
).toMatchInlineSnapshot(`
"const name = \\"John\\";
class Name extends View {
Body() {
return <h1>{name}</h1>;
}
}
export { Name as default };"
`);
});
it('should transform function declaration', () => {
expect(
//language=JSX
transform(`
const name = "John";
function Name() {
function getName() {
return name;
}
const onClick = () => {
console.log(getName());
}
return <h1 onClick={onClick}>{name}</h1>;
}
`)
).toMatchInlineSnapshot(`
"const name = \\"John\\";
class Name extends View {
getName() {
return name;
}
onClick = () => {
console.log(this.getName());
};
Body() {
return <h1 onClick={this.onClick}>{name}</h1>;
}
}"
`);
});
it('should not transform function parameter to this', () => {
expect(
//language=JSX
transform(`
function Name() {
let name = 'Doe'
function getName(name) {
return name + '!'
}
const onClick = () => {
console.log(getName('John'));
}
return <h1 onClick={onClick}>{name}</h1>;
}
`)
).toMatchInlineSnapshot(`
"class Name extends View {
name = 'Doe';
getName(name) {
return name + '!';
}
onClick = () => {
console.log(this.getName('John'));
};
Body() {
return <h1 onClick={this.onClick}>{this.name}</h1>;
}
}"
`);
});
it('should not transform constant data', () => {
expect(
//language=JSX
transform(`
const name = "John";
export default function Name() {
return <h1>{name}</h1>;
}
`)
).toMatchInlineSnapshot(`
"const name = \\"John\\";
class Name extends View {
Body() {
return <h1>{name}</h1>;
}
}
export { Name as default };"
`);
});
it('should transform derived assignment', () => {
expect(
//language=JSX
transform(`
export default function NameComp() {
let firstName = "John";
let lastName = "Doe";
let fullName = \`\${firstName} \${lastName}\`
return <h1>{fullName}</h1>;
}
`)
).toMatchInlineSnapshot(`
"class NameComp extends View {
firstName = \\"John\\";
lastName = \\"Doe\\";
fullName = \`\${this.firstName} \${this.lastName}\`;
Body() {
return <h1>{this.fullName}</h1>;
}
}
export { NameComp as default };"
`);
});
it('should transform watch from call expression', () => {
expect(
//language=JSX
transform(`
export default function CountComp() {
let count = 0;
console.log(count);
return <div>{count}</div>;
}
`)
).toMatchInlineSnapshot(`
"class CountComp extends View {
count = 0;
@Watch
_watch() {
console.log(this.count);
}
Body() {
return <div>{this.count}</div>;
}
}
export { CountComp as default };"
`);
});
it('should transform watch from block statement', () => {
expect(
//language=JSX
transform(`
export default function CountComp() {
let count = 0;
for (let i = 0; i < count; i++) {
console.log(\`The count change to: \${i}\`);
}
return <>
<button onClick={() => count++}>Add</button>
<div>{count}</div>
</>;
};
`)
).toMatchInlineSnapshot(
`
"class CountComp extends View {
count = 0;
@Watch
_watch() {
for (let i = 0; i < this.count; i++) {
console.log(\`The count change to: \${i}\`);
}
}
Body() {
return <>
<button onClick={() => this.count++}>Add</button>
<div>{this.count}</div>
</>;
}
}
export { CountComp as default };
;"
`
);
});
it('should transform watch from if statement', () => {
expect(
//language=JSX
transform(`
export default function CountComp() {
let count = 0;
if (count > 0) {
console.log(\`The count is greater than 0\`);
}
return <div>{count}</div>;
}
`)
).toMatchInlineSnapshot(`
"class CountComp extends View {
count = 0;
@Watch
_watch() {
if (this.count > 0) {
console.log(\`The count is greater than 0\`);
}
}
Body() {
return <div>{this.count}</div>;
}
}
export { CountComp as default };"
`);
});
it('should transform function component reactively', () => {
expect(
transform(`
function MyComp() {
let count = 0
return <>
<h1 count='123'>Hello dlight fn, {count}</h1>
<button onClick={() => count +=1}>Add</button>
<Button />
</>
}`)
).toMatchInlineSnapshot(`
"class MyComp extends View {
count = 0;
Body() {
return <>
<h1 count='123'>Hello dlight fn, {this.count}</h1>
<button onClick={() => this.count += 1}>Add</button>
<Button />
</>;
}
}"
`);
});
});

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { transform as transformWithBabel } from '@babel/core';
import plugin from '../';
export function transform(code: string) {
return transformWithBabel(code, {
presets: [plugin],
})?.code;
}

View File

@ -0,0 +1,115 @@
import { type types as t, NodePath } from '@babel/core';
import * as babel from '@babel/core';
export class ThisPatcher {
private readonly babelApi: typeof babel;
private readonly t: typeof t;
private programNode: t.Program | undefined;
constructor(babelApi: typeof babel) {
this.babelApi = babelApi;
this.t = babelApi.types;
}
patch = (classPath: NodePath<t.Class>) => {
const classBodyNode = classPath.node.body;
const availPropNames = classBodyNode.body
.filter(
(def): def is Exclude<t.ClassBody['body'][number], t.TSIndexSignature | t.StaticBlock> =>
!this.t.isTSIndexSignature(def) && !this.t.isStaticBlock(def)
)
.map(def => (def?.key?.name ? def.key.name : null));
for (const memberOrMethod of classBodyNode.body) {
classPath.scope.traverse(memberOrMethod, {
Identifier: (path: NodePath<t.Identifier>) => {
const idNode = path.node;
if ('key' in memberOrMethod && idNode === memberOrMethod.key) return;
const idName = idNode.name;
if (
availPropNames.includes(idName) &&
!this.isMemberExpression(path) &&
!this.isVariableDeclarator(path) &&
!this.isAttrFromFunction(path, idName, memberOrMethod) &&
!this.isObjectKey(path)
) {
path.replaceWith(this.t.memberExpression(this.t.thisExpression(), this.t.identifier(idName)));
path.skip();
}
},
});
}
};
/**
* check if the identifier is from a function param, e.g:
* class MyClass {
* ok = 1
* myFunc1 = () => ok // change to myFunc1 = () => this.ok
* myFunc2 = ok => ok // don't change !!!!
* }
*/
isAttrFromFunction(path: NodePath<t.Identifier>, idName: string, stopNode: t.ClassBody['body'][number]) {
let reversePath = path.parentPath;
const checkParam = (param: t.Node): boolean => {
// ---- 3 general types:
// * represent allow nesting
// ---0 Identifier: (a)
// ---1 RestElement: (...a) *
// ---1 Pattern: 3 sub Pattern
// -----0 AssignmentPattern: (a=1) *
// -----1 ArrayPattern: ([a, b]) *
// -----2 ObjectPattern: ({a, b})
if (this.t.isIdentifier(param)) return param.name === idName;
if (this.t.isAssignmentPattern(param)) return checkParam(param.left);
if (this.t.isArrayPattern(param)) {
return param.elements.map(el => checkParam(el)).includes(true);
}
if (this.t.isObjectPattern(param)) {
return param.properties.map((prop: any) => prop.key.name).includes(idName);
}
if (this.t.isRestElement(param)) return checkParam(param.argument);
return false;
};
while (reversePath && reversePath.node !== stopNode) {
const node = reversePath.node;
if (this.t.isArrowFunctionExpression(node) || this.t.isFunctionDeclaration(node)) {
for (const param of node.params) {
if (checkParam(param)) return true;
}
}
reversePath = reversePath.parentPath;
}
if (this.t.isClassMethod(stopNode)) {
for (const param of stopNode.params) {
if (checkParam(param)) return true;
}
}
return false;
}
/**
* check if the identifier is already like `this.a` / `xx.a` but not like `a.xx` / xx[a]
*/
isMemberExpression(path: NodePath<t.Identifier>) {
const parentNode = path.parentPath.node;
return this.t.isMemberExpression(parentNode) && parentNode.property === path.node && !parentNode.computed;
}
/**
* check if the identifier is a variable declarator like `let a = 1` `for (let a in array)`
*/
isVariableDeclarator(path: NodePath<t.Identifier>) {
const parentNode = path.parentPath.node;
return this.t.isVariableDeclarator(parentNode) && parentNode.id === path.node;
}
isObjectKey(path: NodePath<t.Identifier>) {
const parentNode = path.parentPath.node;
return this.t.isObjectProperty(parentNode) && parentNode.key === path.node;
}
}

View File

@ -0,0 +1,7 @@
export interface Option {
files?: string | string[];
excludeFiles?: string | string[];
htmlTags?: string[];
parseTemplate?: boolean;
attributeMap?: Record<string, string>;
}

View File

@ -0,0 +1,33 @@
{
"name": "@openinula/error-handler",
"version": "0.0.1",
"author": {
"name": "IanDx",
"email": "iandxssxx@gmail.com"
},
"keywords": [
"dlight.js"
],
"license": "MIT",
"files": [
"dist"
],
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"build": "tsup --sourcemap"
},
"devDependencies": {
"tsup": "^6.7.0",
"typescript": "^5.3.2"
},
"tsup": {
"entry": ["src/index.ts"],
"format": ["cjs", "esm"],
"clean": true,
"dts": true,
"minify": true
}
}

View File

@ -0,0 +1,62 @@
type DLightErrMap = Record<number, string>;
type ErrorMethod<T extends DLightErrMap, G extends string> = {
[K in keyof T as `${G}${K & number}`]: (...args: string[]) => any;
};
/**
* @brief Create error handler by given error space and error maps
* e.g.
* const errHandler = createErrorHandler("DLight", {
* 1: "Cannot find node type: $0, throw"
* }, {
* 1: "This is an error: $0"
* }, {
* 1: "It's a warning"
* })
* errHandler.throw1("div") // -> throw new Error(":D - DLight[throw1]: Cannot find node type: div, throw")
* errHandler.error1("div") // -> console.error(":D - DLight[error1]: This is an error: div")
* errHandler.warn1() // -> console.warn(":D - DLight[warn1]: It's a warning")
* @param errorSpace
* @param throwMap
* @param errorMap
* @param warningMap
* @returns Error handler
*/
export function createErrorHandler<A extends DLightErrMap, B extends DLightErrMap, C extends DLightErrMap>(
errorSpace: string,
throwMap: A = {} as any,
errorMap: B = {} as any,
warningMap: C = {} as any
) {
function handleError(map: DLightErrMap, type: string, func: (msg: string) => any) {
return Object.fromEntries(
Object.entries(map).map(([code, msg]) => [
`${type}${code}`,
(...args: string[]) => {
args.forEach((arg, i) => {
msg = msg.replace(`$${i}`, arg);
});
return func(`:D - ${errorSpace}[${type}${code}]: ${msg}`);
},
])
);
}
const methods: ErrorMethod<A, 'throw'> & ErrorMethod<B, 'error'> & ErrorMethod<C, 'warn'> = {
...handleError(throwMap, 'throw', msg => {
throw new Error(msg);
}),
...handleError(errorMap, 'error', console.error),
...handleError(warningMap, 'warn', console.warn),
} as any;
function notDescribed(type: string) {
return () => `:D ${errorSpace}: ${type} not described`;
}
return {
...methods,
throwUnknown: notDescribed('throw'),
errorUnknown: notDescribed('error'),
warnUnknown: notDescribed('warn'),
};
}

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true
},
"ts-node": {
"esm": true
}
}

View File

@ -0,0 +1,43 @@
{
"name": "jsx-view-parser",
"version": "0.0.1",
"description": "Inula jsx parser",
"author": {
"name": "IanDx",
"email": "iandxssxx@gmail.com"
},
"keywords": [
"inula"
],
"files": [
"dist"
],
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"build": "tsup --sourcemap",
"test": "vitest --ui"
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@types/babel__core": "^7.20.5",
"@vitest/ui": "^1.2.1",
"tsup": "^6.7.0",
"typescript": "^5.3.2",
"@babel/plugin-syntax-jsx": "7.16.7",
"vitest": "^1.2.1"
},
"tsup": {
"entry": [
"src/index.ts"
],
"format": [
"cjs",
"esm"
],
"clean": true,
"dts": true
}
}

View File

@ -0,0 +1,14 @@
import { ViewParser } from './parser';
import type { ViewUnit, ViewParserConfig, AllowedJSXNode } from './types';
/**
* @brief Generate view units from a babel ast
* @param statement
* @param config
* @returns ViewUnit[]
*/
export function parseView(node: AllowedJSXNode, config: ViewParserConfig): ViewUnit[] {
return new ViewParser(config).parse(node);
}
export * from './types';

View File

@ -0,0 +1,582 @@
import type { NodePath, types as t, traverse as tr } from '@babel/core';
import type {
UnitProp,
ViewUnit,
ViewParserConfig,
AllowedJSXNode,
HTMLUnit,
TextUnit,
MutableUnit,
TemplateProp,
Context,
} from './types';
export class ViewParser {
// ---- Namespace and tag name
private readonly htmlNamespace: string = 'html';
private readonly htmlTagNamespace: string = 'tag';
private readonly compTagNamespace: string = 'comp';
private readonly envTagName: string = 'env';
private readonly forTagName: string = 'for';
private readonly ifTagName: string = 'if';
private readonly elseIfTagName: string = 'else-if';
private readonly elseTagName: string = 'else';
private readonly customHTMLProps: string[] = ['ref'];
private readonly config: ViewParserConfig;
private readonly htmlTags: string[];
private readonly willParseTemplate: boolean;
private readonly t: typeof t;
private readonly traverse: typeof tr;
private readonly viewUnits: ViewUnit[] = [];
private context: Context;
/**
* @brief Constructor
* @param config
* @param context
*/
constructor(config: ViewParserConfig, context: Context = { ifElseStack: [] }) {
this.config = config;
this.t = config.babelApi.types;
this.traverse = config.babelApi.traverse;
this.htmlTags = config.htmlTags;
this.willParseTemplate = config.parseTemplate ?? true;
this.context = context;
}
/**
* @brief Parse the node into view units
* @param node
* @returns ViewUnit[]
*/
parse(node: AllowedJSXNode): ViewUnit[] {
if (this.t.isJSXText(node)) this.parseText(node);
else if (this.t.isJSXExpressionContainer(node)) this.parseExpression(node.expression);
else if (this.t.isJSXElement(node)) this.parseElement(node);
else if (this.t.isJSXFragment(node)) {
node.children.forEach(child => {
this.parse(child);
});
}
return this.viewUnits;
}
/**
* @brief Parse JSXText
* @param node
*/
private parseText(node: t.JSXText): void {
const text = node.value.trim();
if (!text) return;
this.viewUnits.push({
type: 'text',
content: this.t.stringLiteral(node.value),
});
}
/**
* @brief Parse JSXExpressionContainer
* @param node
*/
private parseExpression(node: t.Expression | t.JSXEmptyExpression): void {
if (this.t.isJSXEmptyExpression(node)) return;
if (this.t.isLiteral(node) && !this.t.isTemplateLiteral(node)) {
// ---- Treat literal as text except template literal
// Cuz template literal may have viewProp inside like:
// <>{i18n`hello ${<MyView/>}`}</>
this.viewUnits.push({
type: 'text',
content: node,
});
return;
}
this.viewUnits.push({
type: 'exp',
content: this.parseProp(node),
props: {},
});
}
/**
* @brief Parse JSXElement
* @param node
*/
private parseElement(node: t.JSXElement): void {
let type: 'html' | 'comp';
let tag: t.Expression;
// ---- Parse tag and type
const openingName = node.openingElement.name;
if (this.t.isJSXIdentifier(openingName)) {
// ---- Opening name is a JSXIdentifier, e.g., <div>
const name = openingName.name;
// ---- Specially parse if and env
if ([this.ifTagName, this.elseIfTagName, this.elseTagName].includes(name)) return this.parseIf(node);
if (name === this.envTagName) return this.parseEnv(node);
if (name === this.forTagName) return this.pareFor(node);
else if (this.htmlTags.includes(name)) {
type = 'html';
tag = this.t.stringLiteral(name);
} else {
// ---- If the name is not in htmlTags, treat it as a comp
type = 'comp';
tag = this.t.identifier(name);
}
} else if (this.t.isJSXMemberExpression(openingName)) {
// ---- Opening name is a JSXMemberExpression, e.g., <Comp.Div>
// Treat it as a comp and set the tag as the opening name
type = 'comp';
// ---- Turn JSXMemberExpression into MemberExpression recursively
const toMemberExpression = (node: t.JSXMemberExpression): t.MemberExpression => {
if (this.t.isJSXMemberExpression(node.object)) {
return this.t.memberExpression(toMemberExpression(node.object), this.t.identifier(node.property.name));
}
return this.t.memberExpression(this.t.identifier(node.object.name), this.t.identifier(node.property.name));
};
tag = toMemberExpression(openingName);
} else {
// ---- isJSXNamespacedName
const namespace = openingName.namespace.name;
switch (namespace) {
case this.compTagNamespace:
// ---- If the namespace is the same as the compTagNamespace, treat it as a comp
// and set the tag as an identifier
// e.g., <comp:div/> => ["comp", div]
// this means you've declared a component named "div" and force it to be a comp instead an html
type = 'comp';
tag = this.t.identifier(openingName.name.name);
break;
case this.htmlNamespace:
// ---- If the namespace is the same as the htmlTagNamespace, treat it as an html
// and set the tag as a string literal
// e.g., <html:MyWebComponent/> => ["html", "MyWebComponent"]
// the tag will be treated as a string, i.e., <MyWebComponent/>
type = 'html';
tag = this.t.stringLiteral(openingName.name.name);
break;
case this.htmlTagNamespace:
// ---- If the namespace is the same as the htmlTagNamespace, treat it as an html
// and set the tag as an identifier
// e.g., <tag:variable/> => ["html", variable]
// this unit will be htmlUnit and the html string tag is stored in "variable"
type = 'html';
tag = this.t.identifier(openingName.name.name);
break;
default:
// ---- Otherwise, treat it as an html tag and make the tag as the namespace:name
type = 'html';
tag = this.t.stringLiteral(`${namespace}:${openingName.name.name}`);
break;
}
}
// ---- Parse the props
const props = node.openingElement.attributes;
const propMap: Record<string, UnitProp> = Object.fromEntries(props.map(prop => this.parseJSXProp(prop)));
// ---- Parse the children
const childUnits = node.children.map(child => this.parseView(child)).flat();
let unit: ViewUnit = { type, tag, props: propMap, children: childUnits };
if (unit.type === 'html' && childUnits.length === 1 && childUnits[0].type === 'text') {
// ---- If the html unit only has one text child, merge the text into the html unit
const text = childUnits[0] as TextUnit;
unit = {
...unit,
children: [],
props: {
...unit.props,
textContent: {
value: text.content,
viewPropMap: {},
},
},
};
}
if (unit.type === 'html') unit = this.transformTemplate(unit);
this.viewUnits.push(unit);
}
/**
* @brief Parse EnvUnit
* @param node
*/
private parseEnv(node: t.JSXElement): void {
const props = node.openingElement.attributes;
const propMap: Record<string, UnitProp> = Object.fromEntries(props.map(prop => this.parseJSXProp(prop)));
const children = node.children.map(child => this.parseView(child)).flat();
this.viewUnits.push({
type: 'env',
props: propMap,
children,
});
}
private parseIf(node: t.JSXElement): void {
const name = (node.openingElement.name as t.JSXIdentifier).name;
// ---- else
if (name === this.elseTagName) {
const lastUnit = this.context.ifElseStack[this.context.ifElseStack.length - 1];
if (!lastUnit || lastUnit.type !== 'if') throw new Error(`Missing if for ${name}`);
lastUnit.branches.push({
condition: this.t.booleanLiteral(true),
children: node.children.map(child => this.parseView(child)).flat(),
});
this.context.ifElseStack.pop();
return;
}
const condition = node.openingElement.attributes.filter(
attr => this.t.isJSXAttribute(attr) && attr.name.name === 'cond'
)[0];
if (!condition) throw new Error(`Missing condition for ${name}`);
if (!this.t.isJSXAttribute(condition)) throw new Error(`JSXSpreadAttribute is not supported for ${name} condition`);
if (!this.t.isJSXExpressionContainer(condition.value) || !this.t.isExpression(condition.value.expression))
throw new Error(`Invalid condition for ${name}`);
// ---- if
if (name === this.ifTagName) {
const unit = {
type: 'if' as const,
branches: [
{
condition: condition.value.expression,
children: node.children.map(child => this.parseView(child)).flat(),
},
],
};
this.viewUnits.push(unit);
this.context.ifElseStack.push(unit);
return;
}
// ---- else-if
const lastUnit = this.context.ifElseStack[this.context.ifElseStack.length - 1];
if (!lastUnit || lastUnit.type !== 'if') throw new Error(`Missing if for ${name}`);
lastUnit.branches.push({
condition: condition.value.expression,
children: node.children.map(child => this.parseView(child)).flat(),
});
}
/**
* @brief Parse JSXAttribute or JSXSpreadAttribute into UnitProp,
* considering both namespace and expression
* @param prop
* @returns [propName, propValue]
*/
private parseJSXProp(prop: t.JSXAttribute | t.JSXSpreadAttribute): [string, UnitProp] {
if (this.t.isJSXAttribute(prop)) {
let propName: string, specifier: string | undefined;
if (this.t.isJSXNamespacedName(prop.name)) {
// ---- If the prop name is a JSXNamespacedName, e.g., bind:value
// give it a special tag
propName = prop.name.name.name;
specifier = prop.name.namespace.name;
} else {
propName = prop.name.name;
}
let value = this.t.isJSXExpressionContainer(prop.value) ? prop.value.expression : prop.value;
if (this.t.isJSXEmptyExpression(value)) value = undefined;
return [propName, this.parseProp(value, specifier)];
}
// ---- Use *spread* as the propName to avoid conflict with other props
return ['*spread*', this.parseProp(prop.argument)];
}
/**
* @brief Parse the prop node into UnitProp
* @param propNode
* @param specifier
* @returns UnitProp
*/
private parseProp(propNode: t.Expression | undefined | null, specifier?: string): UnitProp {
// ---- If there is no propNode, set the default prop as true
if (!propNode) {
return {
value: this.t.booleanLiteral(true),
viewPropMap: {},
};
}
// ---- Collect sub jsx nodes as Prop
const viewPropMap: Record<string, ViewUnit[]> = {};
const parseViewProp = (innerPath: NodePath<t.JSXElement | t.JSXFragment>): void => {
const id = this.uid();
const node = innerPath.node;
viewPropMap[id] = this.parseView(node);
const newNode = this.t.stringLiteral(id);
if (node === propNode) {
// ---- If the node is the propNode, replace it with the new node
propNode = newNode;
}
// ---- Replace the node and skip the inner path
innerPath.replaceWith(newNode);
innerPath.skip();
};
// ---- Apply the parseViewProp to JSXElement and JSXFragment
this.traverse(this.wrapWithFile(propNode), {
JSXElement: parseViewProp,
JSXFragment: parseViewProp,
});
return {
value: propNode,
viewPropMap,
specifier,
};
}
transformTemplate(unit: ViewUnit): ViewUnit {
if (!this.willParseTemplate) return unit;
if (!this.isHTMLTemplate(unit)) return unit;
unit = unit as HTMLUnit;
return {
type: 'template',
template: this.generateTemplate(unit),
mutableUnits: this.generateMutableUnits(unit),
props: this.parseTemplateProps(unit),
};
}
/**
* @brief Generate the entire HTMLUnit
* @param unit
* @returns HTMLUnit
*/
private generateTemplate(unit: HTMLUnit): HTMLUnit {
const staticProps = Object.fromEntries(
this.filterTemplateProps(
// ---- Get all the static props
Object.entries(unit.props ?? []).filter(
([, prop]) =>
this.isStaticProp(prop) &&
// ---- Filter out props with false values
!(this.t.isBooleanLiteral(prop.value) && !prop.value.value)
)
)
);
let children: (HTMLUnit | TextUnit)[] = [];
if (unit.children) {
children = unit.children
.map(unit => {
if (unit.type === 'text') return unit;
if (unit.type === 'html' && this.t.isStringLiteral(unit.tag)) {
return this.generateTemplate(unit);
}
})
.filter(Boolean) as (HTMLUnit | TextUnit)[];
}
return {
type: 'html',
tag: unit.tag,
props: staticProps,
children,
};
}
/**
* @brief Collect all the mutable nodes in a static HTMLUnit
* We use this function to collect mutable nodes' path and props,
* so that in the generator, we know which position to insert the mutable nodes
* @param htmlUnit
* @returns mutable particles
*/
private generateMutableUnits(htmlUnit: HTMLUnit): MutableUnit[] {
const mutableUnits: MutableUnit[] = [];
const generateMutableUnit = (unit: HTMLUnit, path: number[] = []) => {
const maxHtmlIdx = unit.children?.filter(
child => (child.type === 'html' && this.t.isStringLiteral(child.tag)) || child.type === 'text'
).length;
let htmlIdx = -1;
// ---- Generate mutable unit for current HTMLUnit
unit.children?.forEach(child => {
if (!(child.type === 'html' && this.t.isStringLiteral(child.tag)) && !(child.type === 'text')) {
const idx = htmlIdx + 1 >= maxHtmlIdx ? -1 : htmlIdx + 1;
mutableUnits.push({
path: [...path, idx],
...this.transformTemplate(child),
});
} else {
htmlIdx++;
}
});
// ---- Recursively generate mutable units for static HTMLUnit children
unit.children
?.filter(child => child.type === 'html' && this.t.isStringLiteral(child.tag))
.forEach((child, idx) => {
generateMutableUnit(child as HTMLUnit, [...path, idx]);
});
};
generateMutableUnit(htmlUnit);
return mutableUnits;
}
/**
* @brief Collect all the props in a static HTMLUnit or its nested HTMLUnit children
* Just like the mutable nodes, props are also equipped with path,
* so that we know which HTML ChildNode to insert the props
* @param htmlUnit
* @returns props
*/
private parseTemplateProps(htmlUnit: HTMLUnit): TemplateProp[] {
const templateProps: TemplateProp[] = [];
const generateVariableProp = (unit: HTMLUnit, path: number[]) => {
// ---- Generate all non-static(string/number/boolean) props for current HTMLUnit
// to be inserted further in the generator
unit.props &&
Object.entries(unit.props)
.filter(([, prop]) => !this.isStaticProp(prop))
.forEach(([key, prop]) => {
templateProps.push({
tag: unit.tag,
name: (unit.tag as t.StringLiteral).value,
key,
path,
value: prop.value,
});
});
// ---- Recursively generate props for static HTMLUnit children
unit.children
?.filter(child => child.type === 'html' && this.t.isStringLiteral(child.tag))
.forEach((child, idx) => {
generateVariableProp(child as HTMLUnit, [...path, idx]);
});
};
generateVariableProp(htmlUnit, []);
return templateProps;
}
/**
* @brief Check if a ViewUnit is a static HTMLUnit that can be parsed into a template
* Must satisfy:
* 1. type is html
* 2. tag is a string literal, i.e., non-dynamic tag
* 3. has at least one child that is a static HTMLUnit,
* or else just call a createElement function, no need for template clone
* @param viewUnit
* @returns is a static HTMLUnit
*/
private isHTMLTemplate(viewUnit: ViewUnit): boolean {
return (
viewUnit.type === 'html' &&
this.t.isStringLiteral(viewUnit.tag) &&
!!viewUnit.children?.some(child => child.type === 'html' && this.t.isStringLiteral(child.tag))
);
}
private isStaticProp(prop: UnitProp): boolean {
return (
this.t.isStringLiteral(prop.value) ||
this.t.isNumericLiteral(prop.value) ||
this.t.isBooleanLiteral(prop.value) ||
this.t.isNullLiteral(prop.value)
);
}
/**
* @brief Filter out some props that are not needed in the template,
* these are all special props to be parsed differently in the generator
* @param props
* @returns filtered props
*/
private filterTemplateProps<T>(props: Array<[string, T]>): Array<[string, T]> {
return (
props
// ---- Filter out event listeners
.filter(([key]) => !key.startsWith('on'))
// ---- Filter out specific props
.filter(([key]) => !this.customHTMLProps.includes(key))
);
}
/**
* @brief Parse the view by duplicating current parser's classRootPath, statements and htmlTags
* @param statements
* @returns ViewUnit[]
*/
private parseView(node: AllowedJSXNode): ViewUnit[] {
return new ViewParser({ ...this.config, parseTemplate: false }, this.context).parse(node);
}
/**
* @brief Wrap the value in a file
* @param node
* @returns wrapped value
*/
private wrapWithFile(node: t.Expression): t.File {
return this.t.file(this.t.program([this.t.expressionStatement(node)]));
}
/**
* @brief Generate a unique id
* @returns a unique id
*/
private uid(): string {
return Math.random().toString(36).slice(2);
}
private findProp(node: t.JSXElement, name: string) {
const props = node.openingElement.attributes;
return props.find((prop): prop is t.JSXAttribute => this.t.isJSXAttribute(prop) && prop.name.name === name);
}
private pareFor(node: t.JSXElement) {
// ---- Get array
const arrayContainer = this.findProp(node, 'array');
if (!arrayContainer) throw new Error('Missing [array] prop in for loop');
if (!this.t.isJSXExpressionContainer(arrayContainer.value)) throw new Error('Expected expression container for [array] prop');
const array = arrayContainer.value.expression;
if (this.t.isJSXEmptyExpression(array)) throw new Error('Expected [array] expression not empty');
// ---- Get key
const keyProp = this.findProp(node, 'key');
let key: t.Expression = this.t.nullLiteral();
if (keyProp) {
if (!this.t.isJSXExpressionContainer(keyProp.value)) throw new Error('Expected expression container');
if (this.t.isJSXEmptyExpression(keyProp.value.expression)) throw new Error('Expected expression not empty');
key = keyProp.value.expression;
}
// ---- Get Item
const itemProp = this.findProp(node, 'item');
if (!itemProp) throw new Error('Missing [item] prop in for loop');
if (!this.t.isJSXExpressionContainer(itemProp.value)) throw new Error('Expected expression container for [item] prop');
const item = itemProp.value.expression;
if (this.t.isJSXEmptyExpression(item)) throw new Error('Expected [item] expression not empty');
// ---- ObjectExpression to ObjectPattern / ArrayExpression to ArrayPattern
this.traverse(this.wrapWithFile(item), {
ObjectExpression: (path) => {
path.node.type = 'ObjectPattern' as any;
},
ArrayExpression: (path) => {
path.node.type = 'ArrayPattern' as any;
}
});
// ---- Get children
const children = this.t.jsxFragment(this.t.jsxOpeningFragment(), this.t.jsxClosingFragment(), node.children);
this.viewUnits.push({
type: 'for',
key,
item: item as t.LVal,
array,
children: this.parseView(children)
});
}
}

View File

@ -0,0 +1,189 @@
import { describe, expect, it, afterAll, beforeAll } from 'vitest';
import { config, parse, parseCode, parseView } from './mock';
import { types as t } from '@babel/core';
import type { CompUnit, HTMLUnit } from '../index';
describe('ElementUnit', () => {
beforeAll(() => {
// ---- Ignore template for this test
config.parseTemplate = false;
});
afterAll(() => {
config.parseTemplate = true;
});
// ---- Type
it('should identify a JSX element with tag in htmlTags as an HTMLUnit', () => {
const viewUnits = parse('<div></div>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('html');
});
it('should identify a JSX element with tag not in htmlTags as an CompUnit', () => {
const viewUnits = parse('<Comp></Comp>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('comp');
});
it('should identify a JSX element with namespaced "html" outside htmlTags as an HTMLUnit', () => {
const viewUnits = parse('<html:MyWebComponent></html:MyWebComponent>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('html');
});
it('should identify a JSX element with namespaced "tag" outside htmlTags as an HTMLUnit', () => {
const viewUnits = parse('<tag:variable></tag:variable>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('html');
});
it('should identify a JSX element with namespaced "comp" inside htmlTags as an HTMLUnit', () => {
const viewUnits = parse('<comp:div></comp:div>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('comp');
});
it('should identify a JSX element with name equal to "env" as an EnvUnit', () => {
const viewUnits = parse('<env></env>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('env');
});
// ---- Tag
it('should correctly parse the tag of an HTMLUnit', () => {
const viewUnits = parse('<div></div>');
const tag = (viewUnits[0] as HTMLUnit).tag;
expect(t.isStringLiteral(tag, { value: 'div' })).toBeTruthy();
});
it('should correctly parse the tag of an HTMLUnit with namespaced "html"', () => {
const viewUnits = parse('<html:MyWebComponent></html:MyWebComponent>');
const tag = (viewUnits[0] as HTMLUnit).tag;
expect(t.isStringLiteral(tag, { value: 'MyWebComponent' })).toBeTruthy();
});
it('should correctly parse the tag of an HTMLUnit with namespaced "tag"', () => {
const viewUnits = parse('<tag:variable></tag:variable>');
const tag = (viewUnits[0] as HTMLUnit).tag;
expect(t.isIdentifier(tag, { name: 'variable' })).toBeTruthy();
});
it('should correctly parse the tag of an CompUnit', () => {
const viewUnits = parse('<Comp></Comp>');
const tag = (viewUnits[0] as HTMLUnit).tag;
expect(t.isIdentifier(tag, { name: 'Comp' })).toBeTruthy();
});
it('should correctly parse the tag of an CompUnit with namespaced "comp"', () => {
const viewUnits = parse('<comp:div></comp:div>');
const tag = (viewUnits[0] as HTMLUnit).tag;
expect(t.isIdentifier(tag, { name: 'div' })).toBeTruthy();
});
// ---- Props(for both HTMLUnit and CompUnit)
it('should correctly parse the props', () => {
const viewUnits = parse('<div id="myId"></div>');
const htmlUnit = viewUnits[0] as HTMLUnit;
const props = htmlUnit.props!;
expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy();
});
it('should correctly parse the props with a complex expression', () => {
const ast = parseCode('<div onClick={() => {console.log("ok")}}></div>');
const viewUnits = parseView(ast);
const originalExpression = (
((ast as t.JSXElement).openingElement.attributes[0] as t.JSXAttribute).value as t.JSXExpressionContainer
).expression;
const htmlUnit = viewUnits[0] as HTMLUnit;
expect(htmlUnit.props!.onClick.value).toBe(originalExpression);
});
it('should correctly parse multiple props', () => {
const viewUnits = parse('<div id="myId" class="myClass"></div>');
const htmlUnit = viewUnits[0] as HTMLUnit;
const props = htmlUnit.props!;
expect(Object.keys(props).length).toBe(2);
expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy();
expect(t.isStringLiteral(props.class.value, { value: 'myClass' })).toBeTruthy();
});
it('should correctly parse props with namespace as its specifier', () => {
const viewUnits = parse('<div bind:id="myId"></div>');
const htmlUnit = viewUnits[0] as HTMLUnit;
const props = htmlUnit.props!;
expect(props.id.specifier).toBe('bind');
expect(t.isStringLiteral(props.id.value, { value: 'myId' })).toBeTruthy();
});
it('should correctly parse spread props', () => {
const viewUnits = parse('<Comp {...props}></Comp>');
const htmlUnit = viewUnits[0] as CompUnit;
const props = htmlUnit.props!;
expect(t.isIdentifier(props['*spread*'].value, { name: 'props' })).toBeTruthy();
});
// ---- View prop (other test cases can be found in ExpUnit.test.ts)
it('should correctly parse sub jsx attribute as view prop', () => {
const ast = parseCode('<Comp sub=<div>Ok</div>></Comp>');
const viewUnits = parseView(ast);
const props = (viewUnits[0] as CompUnit).props!;
const viewPropMap = props.sub.viewPropMap!;
expect(Object.keys(viewPropMap).length).toBe(1);
const key = Object.keys(viewPropMap)[0];
const viewProp = viewPropMap[key];
expect(viewProp.length).toBe(1);
expect(viewProp[0].type).toBe('html');
// ---- Prop View will be replaced with a random string and stored in props.viewPropMap
const value = props.sub.value;
expect(t.isStringLiteral(value, { value: key })).toBeTruthy();
});
// ---- Children(for both HTMLUnit and CompUnit)
it('should correctly parse the count of children', () => {
const viewUnits = parse(`<div>
<div>ok</div>
<div>ok</div>
<Comp></Comp>
<Comp></Comp>
</div>`);
const htmlUnit = viewUnits[0] as HTMLUnit;
expect(htmlUnit.children!.length).toBe(4);
});
it('should correctly parse the count of children with JSXExpressionContainer', () => {
const viewUnits = parse(`<div>
<div>ok</div>
<div>ok</div>
{count}
{count}
</div>`);
const htmlUnit = viewUnits[0] as HTMLUnit;
expect(htmlUnit.children!.length).toBe(4);
});
it('should correctly parse the count of children with JSXFragment', () => {
const viewUnits = parse(`<div>
<div>ok</div>
<div>ok</div>
<>
<Comp></Comp>
<Comp></Comp>
</>
</div>`);
const htmlUnit = viewUnits[0] as HTMLUnit;
expect(htmlUnit.children!.length).toBe(4);
});
});

View File

@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest';
import { parse, parseCode, parseView, wrapWithFile } from './mock';
import { types as t } from '@babel/core';
import type { ExpUnit } from '../index';
import { traverse } from '@babel/core';
describe('ExpUnit', () => {
// ---- Type
it('should identify expression unit', () => {
const viewUnits = parse('<>{count}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('exp');
});
it('should not identify literals as expression unit', () => {
const viewUnits = parse('<>{1}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).not.toBe('exp');
});
// ---- Content
it('should correctly parse content for expression unit', () => {
const viewUnits = parse('<>{count}</>');
const content = (viewUnits[0] as ExpUnit).content;
expect(t.isIdentifier(content.value, { name: 'count' })).toBeTruthy();
});
it('should correctly parse complex content for expression unit', () => {
const ast = parseCode('<>{!console.log("hello world") && myComplexFunc(count + 100)}</>');
const viewUnits = parseView(ast);
const originalExpression = ((ast as t.JSXFragment).children[0] as t.JSXExpressionContainer).expression;
const content = (viewUnits[0] as ExpUnit).content;
expect(content.value).toBe(originalExpression);
});
it('should correctly parse content with view prop for expression unit', () => {
// ---- <div>Ok</div> will be replaced with a random string and stored in props.viewPropMap
const viewUnits = parse('<>{<div>Ok</div>}</>');
const content = (viewUnits[0] as ExpUnit).content;
const viewPropMap = content.viewPropMap;
expect(Object.keys(viewPropMap).length).toBe(1);
const key = Object.keys(viewPropMap)[0];
const viewProp = viewPropMap[key];
// ---- Only one view unit for <div>Ok</div>
expect(viewProp.length).toBe(1);
expect(viewProp[0].type).toBe('html');
// ---- The value of the replaced prop should be the key of the viewPropMap
const value = content.value;
expect(t.isStringLiteral(value, { value: key })).toBeTruthy();
});
it('should correctly parse content with view prop for expression unit with complex expression', () => {
// ---- <div>Ok</div> will be replaced with a random string and stored in props.viewPropMap
const ast = parseCode(`<>{
someFunc(() => {
console.log("hello world")
doWhatever()
return <div>Ok</div>
})
}</>`);
const viewUnits = parseView(ast);
const content = (viewUnits[0] as ExpUnit).content;
const viewPropMap = content.viewPropMap;
expect(Object.keys(viewPropMap).length).toBe(1);
const key = Object.keys(viewPropMap)[0];
const viewProp = viewPropMap[key];
// ---- Only one view unit for <div>Ok</div>
expect(viewProp.length).toBe(1);
// ---- Check the value of the replaced prop
let idExistCount = 0;
traverse(wrapWithFile(content.value), {
StringLiteral(path) {
if (path.node.value === key) idExistCount++;
},
});
// ---- Expect the count of the id matching to be exactly 1
expect(idExistCount).toBe(1);
});
});

View File

@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest';
import { parse } from './mock';
describe('ForUnit', () => {
it('should identify for unit', () => {
const viewUnits = parse('<for array={items} item={item}> <div>{item}</div> </for>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('for');
});
});

View File

@ -0,0 +1,201 @@
import { describe, expect, it } from 'vitest';
import { parse } from './mock';
import { types as t } from '@babel/core';
import { HTMLUnit, IfUnit } from '../types';
describe('IfUnit', () => {
// ---- Type
it('should identify if unit', () => {
const viewUnits = parse('<if cond={true}>true</if>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('if');
});
it('should identify if unit with else', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else>false</else>
</>`);
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('if');
});
it('should identify if unit with else-if', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else-if cond={false}>false</else-if>
</>`);
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('if');
});
it('should find matched if in html tag', () => {
const viewUnits = parse(`<section>
<if cond={true}>true</if>
<else>false</else>
</section>`) as unknown as HTMLUnit[];
expect(viewUnits[0].children[0].type).toBe('if');
});
it('should identify if unit with else-if and else', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else-if cond={false}>false</else-if>
<else>else</else>
</>`);
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('if');
});
it('should identify if unit with multiple else-if', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else-if cond={flag1}>flag1</else-if>
<else-if cond={flag2}>flag2</else-if>
<else>else</else>
</>`);
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('if');
});
// ---- Branches
it('should correctly parse branches count for if unit', () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else-if cond={flag1}>flag1</else-if>
<else-if cond={flag2}>flag2</else-if>
<else>else</else>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
expect(branches.length).toBe(4);
});
it("should correctly parse branches' condition for if unit", () => {
const viewUnits = parse('<if cond={true}>true</if>');
const branches = (viewUnits[0] as IfUnit).branches;
expect(t.isBooleanLiteral(branches[0].condition, { value: true })).toBeTruthy();
});
it("should correctly parse branches' children for if unit", () => {
const viewUnits = parse('<if cond={true}>true</if>');
const branches = (viewUnits[0] as IfUnit).branches;
expect(branches[0].children.length).toBe(1);
expect(branches[0].children[0].type).toBe('text');
});
it("should correctly parse branches' condition for if unit with else", () => {
const viewUnits = parse(`<>
<if cond={flag1}>1</if>
<else>2</else>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
expect(t.isIdentifier(branches[0].condition, { name: 'flag1' })).toBeTruthy();
expect(t.isBooleanLiteral(branches[1].condition, { value: true })).toBeTruthy();
});
it("should correctly parse branches' children for if unit with else", () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else>false</else>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
expect(branches[0].children.length).toBe(1);
expect(branches[0].children[0].type).toBe('text');
expect(branches[1].children.length).toBe(1);
expect(branches[1].children[0].type).toBe('text');
});
it("should correctly parse branches' condition for if unit with else-if", () => {
const viewUnits = parse(`<>
<if cond={flag1}>1</if>
<else-if cond={flag2}>2</else-if>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
/**
* () => {
* if (flag1) {
* this._prevCond
* return 1
* } else () {
* if (flag2) {
* }
* }
*/
expect(t.isIdentifier(branches[0].condition, { name: 'flag1' })).toBeTruthy();
expect(t.isIdentifier(branches[1].condition, { name: 'flag2' })).toBeTruthy();
});
it("should correctly parse branches' children for if unit with else-if", () => {
const viewUnits = parse(`<>
<if cond={true}>true</if>
<else-if cond={false}>false</else-if>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
expect(branches[0].children.length).toBe(1);
expect(branches[0].children[0].type).toBe('text');
expect(branches[1].children.length).toBe(1);
expect(branches[1].children[0].type).toBe('text');
});
// --- nested
it('should correctly parse nested if unit', () => {
const viewUnits = parse(`<>
<if cond={true}>
<if cond={true}>true</if>
</if>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
expect(branches.length).toBe(1);
expect(branches[0].children[0].type).toBe('if');
});
it('should correctly parse nested if unit with else', () => {
const viewUnits = parse(`<>
<if cond={true}>
<if cond={true}>true</if>
<else>false</else>
</if>
</>`);
const branches = (viewUnits[0] as IfUnit).branches;
expect(branches.length).toBe(1);
expect(branches[0].children[0].type).toBe('if');
});
it('should throw error for nested if unit with else-if', () => {
expect(() => {
parse(`<>
<if cond={true}>
<else>false</else-if>
</if>
</>`);
}).toThrowError();
});
it('test', () => {
expect(
parse(`
<>
<h1 className="123">Hello dlight fn comp</h1>
<section>
count: {count}, double is: {db}
<button onClick={() => (count += 1)}>Add</button>
</section>
<Button onClick={() => alert(count)}>Alter count</Button>
<h1>Condition</h1>
<if cond={count > 1}>{count} is bigger than is 1</if>
<else>{count} is smaller than 1</else>
<ArrayModification />
</>
`)
);
});
});

View File

@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest';
import { parse } from './mock';
import { types as t } from '@babel/core';
import type { HTMLUnit, TemplateUnit } from '../index';
describe('TemplateUnit', () => {
// ---- Type
it('should not parse a single HTMLUnit to a TemplateUnit', () => {
const viewUnits = parse('<div></div>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('html');
});
it('should parse a nested HTMLUnit to a TemplateUnit', () => {
const viewUnits = parse('<div><div></div></div>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('template');
});
it('should correctly parse a nested HTMLUnit\'s structure into a template', () => {
const viewUnits = parse('<div><div></div></div>');
const template = (viewUnits[0] as TemplateUnit).template;
expect(t.isStringLiteral(template.tag, { value: 'div' })).toBeTruthy();
expect(template.children).toHaveLength(1);
const firstChild = template.children![0] as HTMLUnit;
expect(t.isStringLiteral(firstChild.tag, { value: 'div' })).toBeTruthy();
});
// ---- Props
it('should correctly parse the path of TemplateUnit\'s dynamic props in root element', () => {
const viewUnits = parse('<div class={this.name}><div></div></div>');
const dynamicProps = (viewUnits[0] as TemplateUnit).props;
expect(dynamicProps).toHaveLength(1);
const prop = dynamicProps[0];
expect(prop.path).toHaveLength(0);
});
it('should correctly parse the path of TemplateUnit\'s dynamic props in nested element', () => {
const viewUnits = parse('<div><div class={this.name}></div></div>');
const dynamicProps = (viewUnits[0] as TemplateUnit).props!;
expect(dynamicProps).toHaveLength(1);
const prop = dynamicProps[0]!;
expect(prop.path).toHaveLength(1);
expect(prop.path[0]).toBe(0);
});
it('should correctly parse the path of TemplateUnit\'s dynamic props with mutable particles ahead', () => {
const viewUnits = parse('<div><Comp/><div class={this.name}></div></div>');
const dynamicProps = (viewUnits[0] as TemplateUnit).props!;
expect(dynamicProps).toHaveLength(1);
const prop = dynamicProps[0]!;
expect(prop.path).toHaveLength(1);
expect(prop.path[0]).toBe(0);
});
it('should correctly parse the path of TemplateUnit\'s mutableUnits', () => {
const viewUnits = parse('<div><Comp/><div class={this.name}></div></div>');
const mutableParticles = (viewUnits[0] as TemplateUnit).mutableUnits!;
expect(mutableParticles).toHaveLength(1);
const particle = mutableParticles[0]!;
expect(particle.path).toHaveLength(1);
expect(particle.path[0]).toBe(0);
});
it('should correctly parse the path of multiple TemplateUnit\'s mutableUnits', () => {
const viewUnits = parse('<div><Comp/><div class={this.name}></div><Comp/></div>');
const mutableParticles = (viewUnits[0] as TemplateUnit).mutableUnits!;
expect(mutableParticles).toHaveLength(2);
const firstParticle = mutableParticles[0]!;
expect(firstParticle.path).toHaveLength(1);
expect(firstParticle.path[0]).toBe(0);
const secondParticle = mutableParticles[1]!;
expect(secondParticle.path).toHaveLength(1);
expect(secondParticle.path[0]).toBe(-1);
});
});

View File

@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import { parse } from './mock';
import { types as t } from '@babel/core';
import type { TextUnit } from '../index';
describe('TextUnit', () => {
// ---- Type
it('should identify text unit', () => {
const viewUnits = parse('<>hello world</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('text');
});
it('should identify text unit with boolean expression', () => {
const viewUnits = parse('<>{true}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('text');
});
it('should identify text unit with number expression', () => {
const viewUnits = parse('<>{1}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('text');
});
it('should identify text unit with null expression', () => {
const viewUnits = parse('<>{null}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('text');
});
it('should identify text unit with string literal expression', () => {
const viewUnits = parse('<>{"hello world"}</>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('text');
});
// ---- Content
it('should correctly parse content for text unit', () => {
const viewUnits = parse('<>hello world</>');
const content = (viewUnits[0] as TextUnit).content;
expect(t.isStringLiteral(content, { value: 'hello world' })).toBeTruthy();
});
it('should correctly parse content for boolean text unit', () => {
const viewUnits = parse('<>{true}</>');
const content = (viewUnits[0] as TextUnit).content;
expect(t.isBooleanLiteral(content, { value: true })).toBeTruthy();
});
it('should correctly parse content for number text unit', () => {
const viewUnits = parse('<>{1}</>');
const content = (viewUnits[0] as TextUnit).content;
expect(t.isNumericLiteral(content, { value: 1 })).toBeTruthy();
});
it('should correctly parse content for null text unit', () => {
const viewUnits = parse('<>{null}</>');
const content = (viewUnits[0] as TextUnit).content;
expect(t.isNullLiteral(content)).toBeTruthy();
});
it('should correctly parse content for string literal text unit', () => {
const viewUnits = parse('<>{"hello world"}</>');
const content = (viewUnits[0] as TextUnit).content;
expect(t.isStringLiteral(content)).toBeTruthy();
});
});

View File

@ -0,0 +1 @@
declare module '@babel/plugin-syntax-jsx';

View File

@ -0,0 +1,228 @@
import babel, { parseSync, types as t } from '@babel/core';
import { AllowedJSXNode, ViewParserConfig } from '../types';
import { parseView as pV } from '..';
import babelJSX from '@babel/plugin-syntax-jsx';
const htmlTags = [
'a',
'abbr',
'address',
'area',
'article',
'aside',
'audio',
'b',
'base',
'bdi',
'bdo',
'blockquote',
'body',
'br',
'button',
'canvas',
'caption',
'cite',
'code',
'col',
'colgroup',
'data',
'datalist',
'dd',
'del',
'details',
'dfn',
'dialog',
'div',
'dl',
'dt',
'em',
'embed',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'i',
'iframe',
'img',
'input',
'ins',
'kbd',
'label',
'legend',
'li',
'link',
'main',
'map',
'mark',
'menu',
'meta',
'meter',
'nav',
'noscript',
'object',
'ol',
'optgroup',
'option',
'output',
'p',
'picture',
'pre',
'progress',
'q',
'rp',
'rt',
'ruby',
's',
'samp',
'script',
'section',
'select',
'slot',
'small',
'source',
'span',
'strong',
'style',
'sub',
'summary',
'sup',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'time',
'title',
'tr',
'track',
'u',
'ul',
'var',
'video',
'wbr',
'acronym',
'applet',
'basefont',
'bgsound',
'big',
'blink',
'center',
'dir',
'font',
'frame',
'frameset',
'isindex',
'keygen',
'listing',
'marquee',
'menuitem',
'multicol',
'nextid',
'nobr',
'noembed',
'noframes',
'param',
'plaintext',
'rb',
'rtc',
'spacer',
'strike',
'tt',
'xmp',
'animate',
'animateMotion',
'animateTransform',
'circle',
'clipPath',
'defs',
'desc',
'ellipse',
'feBlend',
'feColorMatrix',
'feComponentTransfer',
'feComposite',
'feConvolveMatrix',
'feDiffuseLighting',
'feDisplacementMap',
'feDistantLight',
'feDropShadow',
'feFlood',
'feFuncA',
'feFuncB',
'feFuncG',
'feFuncR',
'feGaussianBlur',
'feImage',
'feMerge',
'feMergeNode',
'feMorphology',
'feOffset',
'fePointLight',
'feSpecularLighting',
'feSpotLight',
'feTile',
'feTurbulence',
'filter',
'foreignObject',
'g',
'image',
'line',
'linearGradient',
'marker',
'mask',
'metadata',
'mpath',
'path',
'pattern',
'polygon',
'polyline',
'radialGradient',
'rect',
'set',
'stop',
'svg',
'switch',
'symbol',
'text',
'textPath',
'tspan',
'use',
'view',
];
export const config: ViewParserConfig = {
babelApi: babel,
htmlTags,
};
export function parseCode(code: string) {
return (parseSync(code, { plugins: [babelJSX] })!.program.body[0] as t.ExpressionStatement)
.expression as AllowedJSXNode;
}
export function parseView(node: AllowedJSXNode) {
return pV(node, config);
}
export function parse(code: string) {
return parseView(parseCode(code));
}
export function wrapWithFile(node: t.Expression): t.File {
return t.file(t.program([t.expressionStatement(node)]));
}

View File

@ -0,0 +1,86 @@
import type Babel from '@babel/core';
import type { types as t } from '@babel/core';
export interface Context {
ifElseStack: IfUnit[];
}
export interface UnitProp {
value: t.Expression;
viewPropMap: Record<string, ViewUnit[]>;
specifier?: string;
}
export interface TextUnit {
type: 'text';
content: t.Literal;
}
export type MutableUnit = ViewUnit & { path: number[] };
export interface TemplateProp {
tag: t.Expression;
name: string;
key: string;
path: number[];
value: t.Expression;
}
export interface TemplateUnit {
type: 'template';
template: HTMLUnit;
mutableUnits: MutableUnit[];
props: TemplateProp[];
}
export interface HTMLUnit {
type: 'html';
tag: t.Expression;
props: Record<string, UnitProp>;
children: ViewUnit[];
}
export interface CompUnit {
type: 'comp';
tag: t.Expression;
props: Record<string, UnitProp>;
children: ViewUnit[];
}
export interface IfBranch {
condition: t.Expression;
children: ViewUnit[];
}
export interface IfUnit {
type: 'if';
branches: IfBranch[];
}
export interface ExpUnit {
type: 'exp';
content: UnitProp;
props: Record<string, UnitProp>;
}
export interface EnvUnit {
type: 'env';
props: Record<string, UnitProp>;
children: ViewUnit[];
}
export interface ForUnit {
type: 'for';
item: t.LVal;
array: t.Expression;
key: t.Expression;
children: ViewUnit[];
}
export type ViewUnit = TextUnit | HTMLUnit | CompUnit | IfUnit | ExpUnit | EnvUnit | TemplateUnit | ForUnit;
export interface ViewParserConfig {
babelApi: typeof Babel;
htmlTags: string[];
parseTemplate?: boolean;
}
export type AllowedJSXNode = t.JSXElement | t.JSXFragment | t.JSXText | t.JSXExpressionContainer | t.JSXSpreadChild;

Some files were not shown because too many files have changed in this diff Show More