diff --git a/demos/v2/src/App.tsx b/demos/v2/src/App.tsx index e59b4882..4be9e284 100644 --- a/demos/v2/src/App.tsx +++ b/demos/v2/src/App.tsx @@ -14,4 +14,4 @@ function MyComp() { ); } -render('main', MyComp); +render(MyComp, 'main'); diff --git a/demos/v2/src/App.view.tsx b/demos/v2/src/App.view.tsx deleted file mode 100644 index b048a67d..00000000 --- a/demos/v2/src/App.view.tsx +++ /dev/null @@ -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 ( - - ); -} - -function ArrayModification() { - const arr = []; - willMount(() => {}); - return ( -
-

ArrayModification

- {arr.join(',')} - -
- ); -} - -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 ( -
- count: {count}, double is: {doubleCount} - -
- ); -} - -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 ( -
- count: {count}, double is: {doubleCount} - -
- ); -} - -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 ( - <> -

Hello dlight fn comp

-
- count: {count}, double is: {db} - -
- - - - - ); -} - -function ConditionalRendering({ count }) { - return ( -
-

Condition

- 1}>{count} is bigger than is 1 - {count} is equal to 1 - {count} is smaller than 1 -
- ); -} - -render(MyComp, 'main'); diff --git a/packages/transpiler/README.md b/docs/inula-next/README.md similarity index 82% rename from packages/transpiler/README.md rename to docs/inula-next/README.md index 348d0532..39ed8ecb 100644 --- a/packages/transpiler/README.md +++ b/docs/inula-next/README.md @@ -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 {} ``` - -# Watch +# Proposal +## Watch 自动将Statement包裹Watch的反例: ```jsx diff --git a/packages/inula-next/test/props.test.tsx b/packages/inula-next/test/props.test.tsx index 28752c7b..7561f88c 100644 --- a/packages/inula-next/test/props.test.tsx +++ b/packages/inula-next/test/props.test.tsx @@ -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

{alias}

; } @@ -135,6 +136,4 @@ describe('props', () => { `); }); }); - - describe('nested'); }); diff --git a/packages/transpiler/class-transformer/src/pluginProvider.ts b/packages/transpiler/class-transformer/src/pluginProvider.ts index ae799527..0cd62900 100644 --- a/packages/transpiler/class-transformer/src/pluginProvider.ts +++ b/packages/transpiler/class-transformer/src/pluginProvider.ts @@ -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, 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.'); + } + }); } } diff --git a/packages/transpiler/class-transformer/src/test/component-composition.test.ts b/packages/transpiler/class-transformer/src/test/component-composition.test.ts index 3521e128..088b3978 100644 --- a/packages/transpiler/class-transformer/src/test/component-composition.test.ts +++ b/packages/transpiler/class-transformer/src/test/component-composition.test.ts @@ -59,21 +59,21 @@ describe('component-composition', () => { //language=JSX expect( transform(` - function UserProfile({ - name = '', - age = null, - favouriteColors : [{r,g,b}, color2], - isAvailable = false, - }) { - return ( - <> -

My name is {name}!

-

My age is {age}!

-

My favourite colors are {favouriteColors.join(', ')}!

-

I am {isAvailable ? 'available' : 'not available'}

- - ); - }`), + function UserProfile({ + name = '', + age = null, + favouriteColors: [{r, g, b}, color2], + isAvailable = false, + }) { + return ( + <> +

My name is {name}!

+

My age is {age}!

+

My favourite colors are {favouriteColors.join(', ')}!

+

I am {isAvailable ? 'available' : 'not available'}

+ + ); + }`), ` 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 ( -
- {children} -
- ); - }`), + function Card({children}) { + return ( +
+ {children} +
+ ); + }`), ` - 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 + + ; + } + function Child({ name },{ theme }){ + return
{theme}
+ } + `) + ).toMatchInlineSnapshot(` + "class App extends View { + Body() { + return + + ; + } + } + class Child extends View { + @Prop + name; + @Env + theme; + Body() { + return
{this.theme}
; + } + }" + `); + }); + }); 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 (
{content} @@ -141,23 +174,30 @@ describe('component-composition', () => { }`) ).toMatchInlineSnapshot(` "class Card extends View { - @Children - content; - bar; - @Prop - foo = 1; - @Prop - val = 1; - Body() { - return
- {this.content} -
; + @Children + content; + bar; + @Prop + foo = 1; + @Prop + val = 1; + Body() + { + return
+ { + this.content } - @Watch - $$bindNestDestructuring() { - this.bar = this.foo; - } - }" +
+ ; + } + @Watch + $$bindNestDestructuring() + { + this.bar = this.foo; + } + } + " `); }); }); diff --git a/packages/transpiler/class-transformer/src/test/lifecycle.test.ts b/packages/transpiler/class-transformer/src/test/lifecycle.test.ts new file mode 100644 index 00000000..c45ecd39 --- /dev/null +++ b/packages/transpiler/class-transformer/src/test/lifecycle.test.ts @@ -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 ( +
+ ); + }`) + ).toMatchInlineSnapshot(` + "class App extends View { + willMount() { + console.log('willMount'); + } + Body() { + return
; + } + }" + `); + }); + it('should support didMount', () => { + //language=JSX + expect( + transform(` + function App() { + didMount: { + console.log('didMount'); + } + return ( +
+ ); + }`) + ).toMatchInlineSnapshot(` + "class App extends View { + didMount() { + console.log('didMount'); + } + Body() { + return
; + } + }" + `); + }); + + it('should support willUnmount', () => { + //language=JSX + expect( + transform(` + function App() { + willUnmount: { + console.log('willUnmount'); + } + return ( +
+ ); + }`) + ).toMatchInlineSnapshot(` + "class App extends View { + willUnmount() { + console.log('willUnmount'); + } + Body() { + return
; + } + }" + `); + }); + + it('should support didUnmount', () => { + //language=JSX + expect( + transform(` + function App() { + didUnmount: { + console.log('didUnmount'); + } + return ( +
+ ); + }`) + ).toMatchInlineSnapshot(` + "class App extends View { + didUnmount() { + console.log('didUnmount'); + } + Body() { + return
; + } + }" + `); + }); +}); diff --git a/packages/transpiler/class-transformer/src/test/fn2Class.test.ts b/packages/transpiler/class-transformer/src/test/reactivity.test.ts similarity index 55% rename from packages/transpiler/class-transformer/src/test/fn2Class.test.ts rename to packages/transpiler/class-transformer/src/test/reactivity.test.ts index f74a22cf..844e5d27 100644 --- a/packages/transpiler/class-transformer/src/test/fn2Class.test.ts +++ b/packages/transpiler/class-transformer/src/test/reactivity.test.ts @@ -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

{name}

; - } - `) - ).toMatchInlineSnapshot(` + return

{name}

; + } + `) + ).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
count++}>{count}
} `) - ).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

{name}

; - } - `) - ).toMatchInlineSnapshot(` + it('should not transform variable out of scope', () => { + expect( + //language=JSX + transform(` + const name = "John"; + export default function Name() { + return

{name}

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

{name}

; - } - `) - ).toMatchInlineSnapshot(` - "const name = \\"John\\"; - class Name extends View { - Body() { - return

{name}

; - } - } - 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
{count}
; - } - `) - ).toMatchInlineSnapshot(` + return
{count}
; + } + `) + ).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 <> + +
{count}
+ ; + }; + `) + ).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 <> - -
{count}
- ; - }; - `) - ).toMatchInlineSnapshot( + Body() { + return <> + +
{this.count}
+ ; + } + } + 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 <> - -
{this.count}
- ; - } - } - 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
{count}
; - } - `) - ).toMatchInlineSnapshot(` + return
{count}
; + } + `) + ).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 <> -

Hello dlight fn, {count}

- - + - +