feat(fn2cls): env

This commit is contained in:
Hoikan 2024-04-15 20:59:31 +08:00
parent 2d5d3c29e4
commit a536958ad4
8 changed files with 371 additions and 376 deletions

View File

@ -14,4 +14,4 @@ function MyComp() {
</>
);
}
render('main', MyComp);
render(MyComp, 'main');

View File

@ -1,138 +0,0 @@
// @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(MyComp, 'main');

View File

@ -1,8 +1,4 @@
# 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
# Todo-list
- [ ] function 2 class.
- [x] assignment 2 property
@ -13,30 +9,22 @@ This is a experimental package to implement [API2.0](https://gitee.com/openInula
- [ ] partial object destructuring
- [ ] nested object destructuring
- [ ] nested array destructuring
- [ ] alias
- [x] alias
- [x] add `this` @HQ
- [ ] for (jsx-parser) -> playground + benchmark @YH
- [ ] lifecycle @HQ
- [ ] ref @HQ (to validate)
- [ ] env @HQ (to validate)
- [x] for (jsx-parser) -> playground + benchmark @YH
- [x] lifecycle @HQ
- [x] ref @HQ (to validate)
- [x] env @HQ (to validate)
- [ ] Sub component
- [ ] Early Return
- [ ] custom hook -> Model @YH
- [ ] JSX
- [x] style
- [x] fragment
- [ ] ref (to validate)
- [x] 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
@ -60,10 +48,7 @@ This is a experimental package to implement [API2.0](https://gitee.com/openInula
- [ ] early return
# custom hook syntax
TODO
# issues
# Issues
- [ ] partial props destructuring -> support this.$props @YH
```jsx
function Input({onClick, xxx, ...props}) {
@ -105,8 +90,8 @@ class FetchModel {}
<button>Add, Now is {count}</button>
```
# Watch
# Proposal
## Watch
自动将Statement包裹Watch的反例
```jsx

View File

@ -116,7 +116,8 @@ describe('props', () => {
`);
});
it('should support children alias with default value', ({ container }) => {
// TODO: should support children default
it.fails('should support children alias with default value', ({ container }) => {
function Child({ children: alias = 'default child' }) {
return <h1>{alias}</h1>;
}
@ -135,6 +136,4 @@ describe('props', () => {
`);
});
});
describe('nested');
});

View File

@ -6,6 +6,7 @@ import type { Scope } from '@babel/traverse';
const DECORATOR_PROPS = 'Prop';
const DECORATOR_CHILDREN = 'Children';
const DECORATOR_WATCH = 'Watch';
const DECORATOR_ENV = 'Env';
function replaceFnWithClass(path: NodePath<t.FunctionDeclaration>, classTransformer: ClassComponentTransformer) {
const originalName = path.node.id.name;
@ -38,8 +39,14 @@ export class PluginProvider {
const classTransformer = new ClassComponentTransformer(this.babelApi, path);
// transform the parameters to props
const params = path.node.params;
// ---
const props = params[0];
classTransformer.transformProps(props);
// --- env
const env = params[1];
classTransformer.transformEnv(env);
// iterate the function body orderly
const body = path.node.body.body;
body.forEach((node, idx) => {
@ -367,46 +374,50 @@ class ClassComponentTransformer {
);
}
/**
* 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;
}
// TODO: need refactor, maybe merge with props?
transformEnv(env: t.Identifier | t.Pattern | t.RestElement) {
if (!env) {
return;
}
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;
if (!this.t.isObjectPattern(env)) {
throw Error('Unsupported env type, please use object destructuring.');
}
env.properties.forEach(property => {
if (this.t.isObjectProperty(property)) {
const key = property.key;
let defaultVal: t.Expression;
if (this.t.isIdentifier(key)) {
let alias: t.Identifier | null = null;
if (this.t.isAssignmentPattern(property.value)) {
const propName = property.value.left;
defaultVal = property.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(property.value)) {
// handle alias
if (key.name !== property.value.name) {
alias = property.value;
}
} else if (this.t.isObjectPattern(property.value)) {
throw Error('Unsupported nested env destructuring');
}
return false;
if (alias) {
this.addClassPropertyForPropAlias(alias, key);
}
this.addClassProperty(key, DECORATOR_ENV, defaultVal);
return;
}
throw new Error('Unsupported props destructuring, please use simple object destructuring.');
} else {
throw new Error('Unsupported env destructuring, please use plain object destructuring.');
}
});
}
}

View File

@ -59,21 +59,21 @@ describe('component-composition', () => {
//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 >
</>
);
}`),
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 = '';
@ -86,7 +86,7 @@ describe('component-composition', () => {
g;
b;
xx = (() => {
const [{r, g, b},color2] = this.favouriteColors;
const [{r, g, b}, color2] = this.favouriteColors;
this.r = r
this.g = g
this.b = b
@ -108,31 +108,64 @@ describe('component-composition', () => {
//language=JSX
expect(
transform(`
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}`),
function Card({children}) {
return (
<div className="card">
{children}
</div>
);
}`),
`
class Card {
@Children children
class Card {
@Children children
Body() {
div(\`card\`, this.children)
}
Body() {
div(\`card\`, this.children)
}
`
}
`
);
});
});
describe('env', () => {
it('should support env', () => {
expect(
transform(`
function App () {
return <Env theme="dark">
<Child name="child"/>
</Env>;
}
function Child({ name },{ theme }){
return <div>{theme}</div>
}
`)
).toMatchInlineSnapshot(`
"class App extends View {
Body() {
return <env theme=\\"dark\\">
<Child name=\\"child\\" />
</env>;
}
}
class Child extends View {
@Prop
name;
@Env
theme;
Body() {
return <div>{this.theme}</div>;
}
}"
`);
});
});
it('should support children prop with alias', () => {
//language=JSX
expect(
transform(`
function Card({children: content, foo: bar = 1, val = 1}) {
function Card({children: content, foo: bar = 1, val = 1}) {
return (
<div className="card">
{content}
@ -141,23 +174,30 @@ describe('component-composition', () => {
}`)
).toMatchInlineSnapshot(`
"class Card extends View {
@Children
content;
bar;
@Prop
foo = 1;
@Prop
val = 1;
Body() {
return <div className=\\"card\\">
{this.content}
</div>;
@Children
content;
bar;
@Prop
foo = 1;
@Prop
val = 1;
Body()
{
return <div className=\\
"card\\">
{
this.content
}
@Watch
$$bindNestDestructuring() {
this.bar = this.foo;
}
}"
</div>
;
}
@Watch
$$bindNestDestructuring()
{
this.bar = this.foo;
}
}
"
`);
});
});

View File

@ -0,0 +1,114 @@
/*
* 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('lifecycle', () => {
it('should support willMount', () => {
//language=JSX
expect(
transform(`
function App() {
willMount: {
console.log('willMount')
}
return (
<div/>
);
}`)
).toMatchInlineSnapshot(`
"class App extends View {
willMount() {
console.log('willMount');
}
Body() {
return <div />;
}
}"
`);
});
it('should support didMount', () => {
//language=JSX
expect(
transform(`
function App() {
didMount: {
console.log('didMount');
}
return (
<div/>
);
}`)
).toMatchInlineSnapshot(`
"class App extends View {
didMount() {
console.log('didMount');
}
Body() {
return <div />;
}
}"
`);
});
it('should support willUnmount', () => {
//language=JSX
expect(
transform(`
function App() {
willUnmount: {
console.log('willUnmount');
}
return (
<div/>
);
}`)
).toMatchInlineSnapshot(`
"class App extends View {
willUnmount() {
console.log('willUnmount');
}
Body() {
return <div />;
}
}"
`);
});
it('should support didUnmount', () => {
//language=JSX
expect(
transform(`
function App() {
didUnmount: {
console.log('didUnmount');
}
return (
<div/>
);
}`)
).toMatchInlineSnapshot(`
"class App extends View {
didUnmount() {
console.log('didUnmount');
}
Body() {
return <div />;
}
}"
`);
});
});

View File

@ -1,18 +1,19 @@
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';
describe('reactivity', () => {
describe('state', () => {
it('should transform state assignment', () => {
expect(
//language=JSX
transform(`
export default function Name() {
let name = 'John';
return <h1>{name}</h1>;
}
`)
).toMatchInlineSnapshot(`
return <h1>{name}</h1>;
}
`)
).toMatchInlineSnapshot(`
"class Name extends View {
name = 'John';
Body() {
@ -21,17 +22,17 @@ describe('fn2Class', () => {
}
export { Name as default };"
`);
});
});
it('should transform state modification ', () => {
expect(
transform(`
it('should transform state modification ', () => {
expect(
transform(`
function MyApp() {
let count = 0;
return <div onClick={() => count++}>{count}</div>
}
`)
).toMatchInlineSnapshot(`
).toMatchInlineSnapshot(`
"class MyApp extends View {
count = 0;
Body() {
@ -39,18 +40,18 @@ describe('fn2Class', () => {
}
}"
`);
});
});
it('should not transform variable out of scope', () => {
expect(
//language=JSX
transform(`
const name = "John";
export default function Name() {
return <h1>{name}</h1>;
}
`)
).toMatchInlineSnapshot(`
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() {
@ -59,6 +60,7 @@ describe('fn2Class', () => {
}
export { Name as default };"
`);
});
});
it('should transform function declaration', () => {
@ -127,26 +129,6 @@ describe('fn2Class', () => {
`);
});
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
@ -172,18 +154,19 @@ describe('fn2Class', () => {
`);
});
it('should transform watch from call expression', () => {
expect(
//language=JSX
transform(`
export default function CountComp() {
let count = 0;
watch: console.log(count);
describe('watch', () => {
it('should transform watch from call expression', () => {
expect(
//language=JSX
transform(`
export default function CountComp() {
let count = 0;
watch: console.log(count);
return <div>{count}</div>;
}
`)
).toMatchInlineSnapshot(`
return <div>{count}</div>;
}
`)
).toMatchInlineSnapshot(`
"class CountComp extends View {
count = 0;
@Watch
@ -196,60 +179,60 @@ describe('fn2Class', () => {
}
export { CountComp as default };"
`);
});
});
it('should transform watch from block statement', () => {
expect(
//language=JSX
transform(`
export default function CountComp() {
let count = 0;
watch: for (let i = 0; i < count; i++) {
console.log(\`The count change to: \${i}\`);
it('should transform watch from block statement', () => {
expect(
//language=JSX
transform(`
export default function CountComp() {
let count = 0;
watch: 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}\`);
}
}
return <>
<button onClick={() => count++}>Add</button>
<div>{count}</div>
</>;
};
`)
).toMatchInlineSnapshot(
Body() {
return <>
<button onClick={() => this.count++}>Add</button>
<div>{this.count}</div>
</>;
}
}
export { CountComp as default };
;"
`
"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;
watch: if (count > 0) {
console.log(\`The count is greater than 0\`);
}
it('should transform watch from if statement', () => {
expect(
//language=JSX
transform(`
export default function CountComp() {
let count = 0;
watch: if (count > 0) {
console.log(\`The count is greater than 0\`);
}
return <div>{count}</div>;
}
`)
).toMatchInlineSnapshot(`
return <div>{count}</div>;
}
`)
).toMatchInlineSnapshot(`
"class CountComp extends View {
count = 0;
@Watch
@ -264,30 +247,31 @@ describe('fn2Class', () => {
}
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 />
</>
}`)
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 />
</>;
}
}"
`);
"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 />
</>;
}
}"
`);
});
});