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

View File

@ -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>

View File

@ -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 />
</>

View File

@ -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": [

View File

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

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": {
"jsx": "preserve",
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
@ -10,4 +11,4 @@
"ts-node": {
"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/>
```
- [ ] 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.
*/
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>);
},
},
};

View File

@ -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
),

View File

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

View File

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

View File

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