feat: test

This commit is contained in:
Hoikan 2024-04-10 17:58:16 +08:00
parent be4b0cb024
commit 8f60ec6b26
16 changed files with 681 additions and 148 deletions

View File

@ -1,14 +1,53 @@
import { render, View } from '@openinula/next'; import { render, View } from '@openinula/next';
let idCounter = 1; 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 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) { function _random(max) {
return Math.round(Math.random() * 1000) % max; return Math.round(Math.random() * 1000) % max;
} }
function buildData(count) { function buildData(count) {
@ -16,21 +55,23 @@ function buildData(count) {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
data[i] = { data[i] = {
id: idCounter++, 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; return data;
} }
function Button ({ id, text, fn }) { function Button({ id, text, fn }) {
return ( return (
<div class='col-sm-6 smallpad'> <div class="col-sm-6 smallpad">
<button id={ id } class='btn btn-primary btn-block' type='button' onClick={ fn }>{ text }</button> <button id={id} class="btn btn-primary btn-block" type="button" onClick={fn}>
{text}
</button>
</div> </div>
); );
} }
function App () { function App() {
let data = []; let data = [];
let selected = null; let selected = null;
function run() { function run() {
@ -63,29 +104,43 @@ function App () {
} }
return ( return (
<div class='container'> <div class="container">
<div class='jumbotron'><div class='row'> <div class="jumbotron">
<div class='col-md-6'><h1>Inula-next Keyed</h1></div> <div class="row">
<div class='col-md-6'><div class='row'> <div class="col-md-6">
<Button id='run' text='Create 1,000 rows' fn={ run } /> <h1>Inula-next Keyed</h1>
<Button id='runlots' text='Create 10,000 rows' fn={ runLots } /> </div>
<Button id='add' text='Append 1,000 rows' fn={ add } /> <div class="col-md-6">
<Button id='update' text='Update every 10th row' fn={ update } /> <div class="row">
<Button id='clear' text='Clear' fn={ clear } /> <Button id="run" text="Create 1,000 rows" fn={run} />
<Button id='swaprows' text='Swap Rows' fn={ swapRows } /> <Button id="runlots" text="Create 10,000 rows" fn={runLots} />
</div></div> <Button id="add" text="Append 1,000 rows" fn={add} />
</div></div> <Button id="update" text="Update every 10th row" fn={update} />
<table class='table table-hover table-striped test-data'><tbody> <Button id="clear" text="Clear" fn={clear} />
<for array={ data } item={ { id, label } } key={ id }> <Button id="swaprows" text="Swap Rows" fn={swapRows} />
<tr class={ selected === id ? 'danger': '' }> </div>
<td class='col-md-1' textContent={ id } /> </div>
<td class='col-md-4'><a onClick={select.bind(this, id)} textContent={ label } /></td> </div>
<td class='col-md-1'><a onClick={remove.bind(this, id)}><span class='glyphicon glyphicon-remove' aria-hidden="true" /></a></td> </div>
<td class='col-md-6'/> <table class="table table-hover table-striped test-data">
</tr> <tbody>
</for> <for array={data} item={{ id, label }} key={id}>
</tbody></table> <tr class={selected === id ? 'danger' : ''}>
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden="true" /> <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> </div>
); );
} }

View File

@ -7,6 +7,6 @@
</head> </head>
<body> <body>
<div id="main"></div> <div id="main"></div>
<script type="module" src="/src/App.tsx"></script> <script type="module" src="/src/App.view.tsx"></script>
</body> </body>
</html> </html>

View File

@ -16,7 +16,9 @@ import {
} from '@openinula/next'; } from '@openinula/next';
// @ts-ignore // @ts-ignore
function Button({ children, onClick }) { function Button({ children: content, onClick }) {
console.log(content);
return ( return (
<button <button
onClick={onClick} onClick={onClick}
@ -29,11 +31,10 @@ function Button({ children, onClick }) {
borderRadius: '4px', borderRadius: '4px',
}} }}
> >
{children} {content}
</button> </button>
); );
} }
function ArrayModification() { function ArrayModification() {
const arr = []; const arr = [];
willMount(() => {}); 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() { function MyComp() {
let count = 0; let count = 0;
const db = count * 2;
{
console.log(count);
const i = count * 2;
console.log(i);
}
console.log(count);
const i = count * 2;
console.log(i);
const XX = () => {
};
return ( return (
<> <>
@ -122,7 +58,7 @@ function MyComp() {
count: {count}, double is: {db} count: {count}, double is: {db}
<button onClick={() => (count += 1)}>Add</button> <button onClick={() => (count += 1)}>Add</button>
</section> </section>
<Button onClick={() => alert(count)}>Alter count</Button> <Button onClick={() => alert(count)}>{count}</Button>
<ConditionalRendering count={count} /> <ConditionalRendering count={count} />
<ArrayModification /> <ArrayModification />
</> </>

View File

@ -18,14 +18,17 @@
"module": "dist/index.js", "module": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
"scripts": { "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": { "dependencies": {
"csstype": "^3.1.3", "csstype": "^3.1.3",
"@openinula/store": "workspace:*" "@openinula/store": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"tsup": "^6.5.0" "tsup": "^6.5.0",
"vite-plugin-inula-next": "workspace:*",
"vitest": "^1.2.2"
}, },
"tsup": { "tsup": {
"entry": [ "entry": [

View File

@ -22,7 +22,7 @@ function initStore() {
DLStore.global.DidUnmountStore = []; DLStore.global.DidUnmountStore = [];
} }
export function render(idOrEl, DL) { export function render(DL, idOrEl) {
let el = idOrEl; let el = idOrEl;
if (typeof idOrEl === 'string') { if (typeof idOrEl === 'string') {
const elFound = DLStore.document.getElementById(idOrEl); const elFound = DLStore.document.getElementById(idOrEl);

View File

@ -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');
});

View File

@ -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>
`);
});
});
});

View File

@ -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();
},
});

View File

@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"jsx": "preserve",
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"lib": ["ESNext", "DOM"], "lib": ["ESNext", "DOM"],
@ -10,4 +11,4 @@
"ts-node": { "ts-node": {
"esm": true "esm": true
} }
} }

View File

@ -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'
},
});

View File

@ -99,3 +99,34 @@ class FetchModel {}
} }
// <H1/> // <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);
}
}
```

View File

@ -13,7 +13,7 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import { PluginObj } from '@babel/core'; import { NodePath, PluginObj } from '@babel/core';
import { Option } from './types'; import { Option } from './types';
import * as babel from '@babel/core'; import * as babel from '@babel/core';
import { PluginProvider } from './pluginProvider'; import { PluginProvider } from './pluginProvider';
@ -28,7 +28,7 @@ export default function (api: typeof babel, options: Option): PluginObj {
FunctionDeclaration(path) { FunctionDeclaration(path) {
pluginProvider.functionDeclarationVisitor(path); pluginProvider.functionDeclarationVisitor(path);
thisPatcher.patch(path); thisPatcher.patch(path as unknown as NodePath<babel.types.Class>);
}, },
}, },
}; };

View File

@ -3,6 +3,10 @@ import * as babel from '@babel/core';
import { Option } from './types'; import { Option } from './types';
import type { Scope } from '@babel/traverse'; 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) { function replaceFnWithClass(path: NodePath<t.FunctionDeclaration>, classTransformer: ClassComponentTransformer) {
const originalName = path.node.id.name; const originalName = path.node.id.name;
const tempName = path.node.id.name + 'Temp'; const tempName = path.node.id.name + 'Temp';
@ -50,7 +54,7 @@ export class PluginProvider {
} }
// handle watch // handle watch
if (classTransformer.shouldTransformWatch(node)) { if (this.t.isStatement(node)) {
// transform the watch statement to watch method // transform the watch statement to watch method
classTransformer.transformWatch(node); classTransformer.transformWatch(node);
return; return;
@ -85,10 +89,16 @@ type ToWatchNode =
class ClassComponentTransformer { class ClassComponentTransformer {
properties: (t.ClassProperty | t.ClassMethod)[] = []; 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 babelApi: typeof babel;
private readonly t: typeof t; private readonly t: typeof t;
private readonly functionScope: Scope; 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) { addProperty(prop: t.ClassProperty | t.ClassMethod, name?: string) {
this.properties.push(prop); this.properties.push(prop);
} }
@ -102,10 +112,23 @@ class ClassComponentTransformer {
// transform function component to class component extends View // transform function component to class component extends View
genClassComponent(name: string) { 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( return this.t.classDeclaration(
this.t.identifier(name), this.t.identifier(name),
this.t.identifier('View'), 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 // transform node to method with watch decorator
transformWatch(node: ToWatchNode) { 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); 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); this.addProperty(method);
} }
@ -194,36 +217,85 @@ class ClassComponentTransformer {
return this.t.isObjectPattern(param); 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) { private transformPropsDestructuring(param: t.ObjectPattern) {
const propNames: t.Identifier[] = []; const propNames: t.Identifier[] = [];
param.properties.forEach(prop => { param.properties.forEach(prop => {
if (this.t.isObjectProperty(prop)) { if (this.t.isObjectProperty(prop)) {
const key = prop.key; let key = prop.key;
let defaultVal: t.Expression;
if (this.t.isIdentifier(key)) { if (this.t.isIdentifier(key)) {
let alias: t.Identifier;
if (this.t.isAssignmentPattern(prop.value)) { if (this.t.isAssignmentPattern(prop.value)) {
// handle default value const propName = prop.value.left;
const defaultValue = prop.value.right; defaultVal = prop.value.right;
this.addProp(key, defaultValue); if (this.t.isIdentifier(propName)) {
propNames.push(key); // handle alias
return; 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)) { } else if (this.t.isIdentifier(prop.value)) {
// handle simple destructuring // handle alias
this.addProp(key, undefined, prop.value.name === 'children'); if (key.name !== prop.value.name) {
propNames.push(key); alias = prop.value;
return; }
} else if (this.t.isObjectPattern(prop.value)) { } else if (this.t.isObjectPattern(prop.value)) {
// TODO: handle nested destructuring // TODO: handle nested destructuring
this.transformPropsDestructuring(prop.value); 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; return;
} }
// handle default value // handle default value
if (this.t.isAssignmentPattern(prop.value)) { if (this.t.isAssignmentPattern(prop.value)) {
const defaultValue = prop.value.right; const defaultValue = prop.value.right;
const propName = prop.value.left; 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)) { if (this.t.isIdentifier(propName)) {
this.addProp(propName, defaultValue); this.addClassProperty(propName, DECORATOR_PROPS, defaultValue);
propNames.push(propName); propNames.push(propName);
} }
// TODO: handle nested destructuring // TODO: handle nested destructuring
@ -238,8 +310,17 @@ class ClassComponentTransformer {
return propNames; 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 = ''; // 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 // clone the key to avoid reference issue
const id = this.t.cloneNode(key); const id = this.t.cloneNode(key);
this.addProperty( this.addProperty(
@ -248,7 +329,7 @@ class ClassComponentTransformer {
defaultValue ?? undefined, defaultValue ?? undefined,
undefined, undefined,
// use prop decorator // use prop decorator
[this.t.decorator(this.t.identifier(isChildren ? 'Children' : 'Prop'))], decorator ? [this.t.decorator(this.t.identifier(decorator))] : undefined,
undefined, undefined,
false false
), ),

View File

@ -132,22 +132,32 @@ describe('component-composition', () => {
//language=JSX //language=JSX
expect( expect(
transform(` transform(`
function Card({ children: content }) { function Card({children: content, foo: bar = 1, val = 1}) {
return ( return (
<div className="card"> <div className="card">
{children} {content}
</div> </div>
); );
}`), }`)
` ).toMatchInlineSnapshot(`
class Card { "class Card extends View {
@Children content @Children
content;
Body() { bar;
div(\`card\`, this.children) @Prop
} foo = 1;
@Prop
val = 1;
Body() {
return <div className=\\"card\\">
{this.content}
</div>;
} }
` @Watch
); $$bindNestDestructuring() {
this.bar = this.foo;
}
}"
`);
}); });
}); });

View File

@ -39,7 +39,6 @@
"esm" "esm"
], ],
"clean": true, "clean": true,
"dts": true, "dts": true
"minify": true
} }
} }

View File

@ -2,6 +2,7 @@ import { transform } from '@babel/core';
import dlight, { type DLightOption } from 'babel-preset-inula-next'; import dlight, { type DLightOption } from 'babel-preset-inula-next';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import { Plugin, TransformResult } from 'vite'; import { Plugin, TransformResult } from 'vite';
export default function (options: DLightOption = {}): Plugin { export default function (options: DLightOption = {}): Plugin {
const { const {
files: preFiles = '**/*.{js,jsx,ts,tsx}', files: preFiles = '**/*.{js,jsx,ts,tsx}',
@ -28,13 +29,18 @@ export default function (options: DLightOption = {}): Plugin {
} }
} }
if (!enter) return; if (!enter) return;
return transform(code, { try {
babelrc: false, return transform(code, {
configFile: false, babelrc: false,
presets: [[dlight, options]], configFile: false,
sourceMaps: true, presets: [[dlight, options]],
filename: id, sourceMaps: true,
}) as TransformResult; filename: id,
}) as TransformResult;
} catch (err) {
console.error('Error:', err);
}
return '';
}, },
}; };
} }