feat: test
This commit is contained in:
parent
be4b0cb024
commit
8f60ec6b26
|
@ -1,14 +1,53 @@
|
|||
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 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'];
|
||||
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;
|
||||
return Math.round(Math.random() * 1000) % max;
|
||||
}
|
||||
|
||||
function buildData(count) {
|
||||
|
@ -16,21 +55,23 @@ function buildData(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)]}`
|
||||
label: `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`,
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function Button ({ id, text, fn }) {
|
||||
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 class="col-sm-6 smallpad">
|
||||
<button id={id} class="btn btn-primary btn-block" type="button" onClick={fn}>
|
||||
{text}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App () {
|
||||
function App() {
|
||||
let data = [];
|
||||
let selected = null;
|
||||
function run() {
|
||||
|
@ -63,29 +104,43 @@ function App () {
|
|||
}
|
||||
|
||||
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 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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script type="module" src="/src/App.tsx"></script>
|
||||
<script type="module" src="/src/App.view.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -16,7 +16,9 @@ import {
|
|||
} from '@openinula/next';
|
||||
|
||||
// @ts-ignore
|
||||
function Button({ children, onClick }) {
|
||||
function Button({ children: content, onClick }) {
|
||||
console.log(content);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
|
@ -29,11 +31,10 @@ function Button({ children, onClick }) {
|
|||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ArrayModification() {
|
||||
const arr = [];
|
||||
willMount(() => {});
|
||||
|
@ -46,74 +47,9 @@ function ArrayModification() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
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 = () => {
|
||||
|
||||
};
|
||||
const db = count * 2;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -122,7 +58,7 @@ function MyComp() {
|
|||
count: {count}, double is: {db}
|
||||
<button onClick={() => (count += 1)}>Add</button>
|
||||
</section>
|
||||
<Button onClick={() => alert(count)}>Alter count</Button>
|
||||
<Button onClick={() => alert(count)}>{count}</Button>
|
||||
<ConditionalRendering count={count} />
|
||||
<ArrayModification />
|
||||
</>
|
||||
|
|
|
@ -18,14 +18,17 @@
|
|||
"module": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup --sourcemap && cp src/index.d.ts dist/ && cp -r src/types dist/"
|
||||
"build": "tsup --sourcemap && cp src/index.d.ts dist/ && cp -r src/types dist/",
|
||||
"test": "vitest --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.3",
|
||||
"@openinula/store": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^6.5.0"
|
||||
"tsup": "^6.5.0",
|
||||
"vite-plugin-inula-next": "workspace:*",
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": [
|
||||
|
|
|
@ -22,7 +22,7 @@ function initStore() {
|
|||
DLStore.global.DidUnmountStore = [];
|
||||
}
|
||||
|
||||
export function render(idOrEl, DL) {
|
||||
export function render(DL, idOrEl) {
|
||||
let el = idOrEl;
|
||||
if (typeof idOrEl === 'string') {
|
||||
const elFound = DLStore.document.getElementById(idOrEl);
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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, vi } from 'vitest';
|
||||
import { domTest as it } from './utils';
|
||||
import { render, View } from '../src';
|
||||
|
||||
describe('props', () => {
|
||||
describe('normal props', () => {
|
||||
it('should support prop', ({ container }) => {
|
||||
function Child({ name }) {
|
||||
return <h1>{name}</h1>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <Child name={'hello world!!!'} />;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<h1>
|
||||
hello world!!!
|
||||
</h1>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support prop alias', ({ container }) => {
|
||||
function Child({ name: alias }) {
|
||||
return <h1>{alias}</h1>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <Child name={'prop alias'} />;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<h1>
|
||||
prop alias
|
||||
</h1>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support prop alias with default value', ({ container }) => {
|
||||
function Child({ name: alias = 'default' }) {
|
||||
return <h1>{alias}</h1>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <Child />;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<h1>
|
||||
default
|
||||
</h1>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('children', () => {
|
||||
it('should support children', ({ container }) => {
|
||||
function Child({ children }) {
|
||||
return <h1>{children}</h1>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <Child>child content</Child>;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<h1>
|
||||
child content
|
||||
</h1>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support children alias', ({ container }) => {
|
||||
function Child({ children: alias }) {
|
||||
return <h1>{alias}</h1>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <Child>children alias</Child>;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<h1>
|
||||
children alias
|
||||
</h1>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support children alias with default value', ({ container }) => {
|
||||
function Child({ children: alias = 'default child' }) {
|
||||
return <h1>{alias}</h1>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <Child />;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<h1>
|
||||
default child
|
||||
</h1>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested');
|
||||
});
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* 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, vi } from 'vitest';
|
||||
import { domTest as it } from './utils';
|
||||
import { render, View } from '../src';
|
||||
|
||||
describe('rendering', () => {
|
||||
describe('basic', () => {
|
||||
it('should support basic dom', ({ container }) => {
|
||||
function App() {
|
||||
return <h1>hello world!!!</h1>;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<h1>
|
||||
hello world!!!
|
||||
</h1>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support text and variable mixing', ({ container }) => {
|
||||
function App() {
|
||||
const name = 'world';
|
||||
return <h1>hello {name}!!!</h1>;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<h1>
|
||||
hello
|
||||
world
|
||||
!!!
|
||||
</h1>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
// TODO: SHOULD FIX
|
||||
it('should support dom has multiple layers ', ({ container }) => {
|
||||
function App() {
|
||||
let count = 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
Header
|
||||
<h1>hello world!!!</h1>
|
||||
<section>
|
||||
<button>Add, Now is {count}</button>
|
||||
</section>
|
||||
Footer
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
hello world!!!
|
||||
</h1>
|
||||
<section>
|
||||
<button>
|
||||
Add, Now is
|
||||
0
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
// TODO: SHOULD FIX
|
||||
it('should support tag, text and variable mixing', ({ container }) => {
|
||||
function App() {
|
||||
let count = 'world';
|
||||
|
||||
return (
|
||||
<section>
|
||||
count: {count}
|
||||
<button>Add, count is {count}</button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
expect(container).toMatchInlineSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('style', () => {
|
||||
it('should apply styles correctly', ({ container }) => {
|
||||
function App() {
|
||||
return <h1 style={{ color: 'red' }}>hello world!!!</h1>;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
const h1 = container.querySelector('h1');
|
||||
expect(h1.style.color).toBe('red');
|
||||
});
|
||||
|
||||
it('should apply multiple styles correctly', ({ container }) => {
|
||||
function App() {
|
||||
return <h1 style={{ color: 'red', fontSize: '20px' }}>hello world!!!</h1>;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
const h1 = container.querySelector('h1');
|
||||
expect(h1.style.color).toBe('red');
|
||||
expect(h1.style.fontSize).toBe('20px');
|
||||
});
|
||||
|
||||
it('should override styles correctly', ({ container }) => {
|
||||
function App() {
|
||||
return (
|
||||
<h1 style={{ color: 'red' }}>
|
||||
<span style={{ color: 'blue' }}>hello world!!!</span>
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
const span = container.querySelector('span');
|
||||
expect(span.style.color).toBe('blue');
|
||||
});
|
||||
|
||||
it('should handle dynamic styles', ({ container }) => {
|
||||
const color = 'red';
|
||||
|
||||
function App() {
|
||||
return <h1 style={{ color }}>hello world!!!</h1>;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
const h1 = container.querySelector('h1');
|
||||
expect(h1.style.color).toBe('red');
|
||||
});
|
||||
});
|
||||
|
||||
describe('event', () => {
|
||||
it('should handle click events', ({ container }) => {
|
||||
const handleClick = vi.fn();
|
||||
|
||||
function App() {
|
||||
return <button onClick={handleClick}>Click me</button>;
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
const button = container.querySelector('button');
|
||||
button.click();
|
||||
|
||||
expect(handleClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('components', () => {
|
||||
it('should render components', ({ container }) => {
|
||||
function Button({ children }) {
|
||||
return <button>{children}</button>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<h1>hello world!!!</h1>
|
||||
<Button>Click me</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(App, container);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
hello world!!!
|
||||
</h1>
|
||||
<button>
|
||||
Click me
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 } from 'vitest';
|
||||
|
||||
interface DomTestContext {
|
||||
container: HTMLDivElement;
|
||||
}
|
||||
|
||||
// Define a new test type that extends the default test type and adds the container fixture.
|
||||
export const domTest = test.extend<DomTestContext>({
|
||||
container: async ({ task }, use) => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
await use(container);
|
||||
container.remove();
|
||||
},
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
|
@ -10,4 +11,4 @@
|
|||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import inula from 'vite-plugin-inula-next';
|
||||
import * as path from 'node:path';
|
||||
|
||||
export default defineConfig({
|
||||
esbuild: {
|
||||
jsx: 'preserve',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@openinula/next': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
conditions: ['dev'],
|
||||
},
|
||||
plugins: [
|
||||
// @ts-expect-error TODO: fix vite plugin interface is not compatible
|
||||
inula(),
|
||||
],
|
||||
test: {
|
||||
environment: 'jsdom', // or 'jsdom', 'node'
|
||||
},
|
||||
});
|
|
@ -99,3 +99,34 @@ class FetchModel {}
|
|||
}
|
||||
// <H1/>
|
||||
```
|
||||
- [ ] Render text and variable, Got Error
|
||||
```jsx
|
||||
// Uncaught DOMException: Failed to execute 'appendChild' on 'Node': This node type does not support this method.
|
||||
<button>Add, Now is {count}</button>
|
||||
```
|
||||
|
||||
|
||||
# Watch
|
||||
|
||||
自动将Statement包裹Watch的反例:
|
||||
```jsx
|
||||
// 前置操作: 场景为Table组件,需要响应column变化,先置空column,再计算新的columnByKey
|
||||
let columnByKey;
|
||||
watch: {
|
||||
columnByKey = {};
|
||||
columns.forEach(col => {
|
||||
columnByKey[col.key] = col;
|
||||
});
|
||||
}
|
||||
// 临时变量: 场景为操作前的计算部分临时变量
|
||||
watch: {
|
||||
let col = columnByKey[sortBy];
|
||||
if (
|
||||
col !== undefined &&
|
||||
col.sortable === true &&
|
||||
typeof col.value === "function"
|
||||
) {
|
||||
sortFunction = r => col.value(r);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { PluginObj } from '@babel/core';
|
||||
import { NodePath, PluginObj } from '@babel/core';
|
||||
import { Option } from './types';
|
||||
import * as babel from '@babel/core';
|
||||
import { PluginProvider } from './pluginProvider';
|
||||
|
@ -28,7 +28,7 @@ export default function (api: typeof babel, options: Option): PluginObj {
|
|||
FunctionDeclaration(path) {
|
||||
pluginProvider.functionDeclarationVisitor(path);
|
||||
|
||||
thisPatcher.patch(path);
|
||||
thisPatcher.patch(path as unknown as NodePath<babel.types.Class>);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,6 +3,10 @@ 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';
|
||||
|
@ -50,7 +54,7 @@ export class PluginProvider {
|
|||
}
|
||||
|
||||
// handle watch
|
||||
if (classTransformer.shouldTransformWatch(node)) {
|
||||
if (this.t.isStatement(node)) {
|
||||
// transform the watch statement to watch method
|
||||
classTransformer.transformWatch(node);
|
||||
return;
|
||||
|
@ -85,10 +89,16 @@ type ToWatchNode =
|
|||
|
||||
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);
|
||||
}
|
||||
|
@ -102,10 +112,23 @@ class ClassComponentTransformer {
|
|||
|
||||
// transform function component to class component extends View
|
||||
genClassComponent(name: string) {
|
||||
// generate ctor and push this.initExpressions to ctor
|
||||
let nestedDestructuringBindingsMethod: t.ClassMethod;
|
||||
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(this.properties),
|
||||
this.t.classBody(
|
||||
nestedDestructuringBindingsMethod ? [...this.properties, nestedDestructuringBindingsMethod] : this.properties
|
||||
),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
@ -184,9 +207,9 @@ class ClassComponentTransformer {
|
|||
|
||||
// transform node to method with watch decorator
|
||||
transformWatch(node: ToWatchNode) {
|
||||
const id = this.functionScope.generateUidIdentifier('watch');
|
||||
const id = this.functionScope.generateUidIdentifier(DECORATOR_WATCH.toLowerCase());
|
||||
const method = this.t.classMethod('method', id, [], this.t.blockStatement([node]), false, false);
|
||||
method.decorators = [this.t.decorator(this.t.identifier('Watch'))];
|
||||
method.decorators = [this.t.decorator(this.t.identifier(DECORATOR_WATCH))];
|
||||
this.addProperty(method);
|
||||
}
|
||||
|
||||
|
@ -194,36 +217,85 @@ class ClassComponentTransformer {
|
|||
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)) {
|
||||
const key = prop.key;
|
||||
let key = prop.key;
|
||||
let defaultVal: t.Expression;
|
||||
if (this.t.isIdentifier(key)) {
|
||||
let alias: t.Identifier;
|
||||
if (this.t.isAssignmentPattern(prop.value)) {
|
||||
// handle default value
|
||||
const defaultValue = prop.value.right;
|
||||
this.addProp(key, defaultValue);
|
||||
propNames.push(key);
|
||||
return;
|
||||
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 simple destructuring
|
||||
this.addProp(key, undefined, prop.value.name === 'children');
|
||||
propNames.push(key);
|
||||
return;
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
|
||||
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.addProp(propName, defaultValue);
|
||||
this.addClassProperty(propName, DECORATOR_PROPS, defaultValue);
|
||||
propNames.push(propName);
|
||||
}
|
||||
// TODO: handle nested destructuring
|
||||
|
@ -238,8 +310,17 @@ class ClassComponentTransformer {
|
|||
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 addProp(key: t.Identifier, defaultValue?: t.Expression, isChildren = false) {
|
||||
private addClassProperty(key: t.Identifier, decorator: string, defaultValue?: t.Expression) {
|
||||
// clone the key to avoid reference issue
|
||||
const id = this.t.cloneNode(key);
|
||||
this.addProperty(
|
||||
|
@ -248,7 +329,7 @@ class ClassComponentTransformer {
|
|||
defaultValue ?? undefined,
|
||||
undefined,
|
||||
// use prop decorator
|
||||
[this.t.decorator(this.t.identifier(isChildren ? 'Children' : 'Prop'))],
|
||||
decorator ? [this.t.decorator(this.t.identifier(decorator))] : undefined,
|
||||
undefined,
|
||||
false
|
||||
),
|
||||
|
|
|
@ -132,22 +132,32 @@ describe('component-composition', () => {
|
|||
//language=JSX
|
||||
expect(
|
||||
transform(`
|
||||
function Card({ children: content }) {
|
||||
function Card({children: content, foo: bar = 1, val = 1}) {
|
||||
return (
|
||||
<div className="card">
|
||||
{children}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}`),
|
||||
`
|
||||
class Card {
|
||||
@Children content
|
||||
|
||||
Body() {
|
||||
div(\`card\`, this.children)
|
||||
}
|
||||
}`)
|
||||
).toMatchInlineSnapshot(`
|
||||
"class Card extends View {
|
||||
@Children
|
||||
content;
|
||||
bar;
|
||||
@Prop
|
||||
foo = 1;
|
||||
@Prop
|
||||
val = 1;
|
||||
Body() {
|
||||
return <div className=\\"card\\">
|
||||
{this.content}
|
||||
</div>;
|
||||
}
|
||||
`
|
||||
);
|
||||
@Watch
|
||||
$$bindNestDestructuring() {
|
||||
this.bar = this.foo;
|
||||
}
|
||||
}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
"esm"
|
||||
],
|
||||
"clean": true,
|
||||
"dts": true,
|
||||
"minify": true
|
||||
"dts": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { transform } from '@babel/core';
|
|||
import dlight, { type DLightOption } from 'babel-preset-inula-next';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { Plugin, TransformResult } from 'vite';
|
||||
|
||||
export default function (options: DLightOption = {}): Plugin {
|
||||
const {
|
||||
files: preFiles = '**/*.{js,jsx,ts,tsx}',
|
||||
|
@ -28,13 +29,18 @@ export default function (options: DLightOption = {}): Plugin {
|
|||
}
|
||||
}
|
||||
if (!enter) return;
|
||||
return transform(code, {
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
presets: [[dlight, options]],
|
||||
sourceMaps: true,
|
||||
filename: id,
|
||||
}) as TransformResult;
|
||||
try {
|
||||
return transform(code, {
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
presets: [[dlight, options]],
|
||||
sourceMaps: true,
|
||||
filename: id,
|
||||
}) as TransformResult;
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue