diff --git a/packages/inula-novdom/README.md b/packages/inula-novdom/README.md index d90523e3..3c98b086 100644 --- a/packages/inula-novdom/README.md +++ b/packages/inula-novdom/README.md @@ -1,562 +1,6 @@ -# **Templating** - -## JSX - -### template - -JSX整体编译为template模板,动态部分通过insert插入。 -template字符串不进行省略(end tag)。待讨论 - -```jsx -
Count value is .
- -// solid -const $tmpl = /*#__PURE__*/ $$template(`
Count value is .`); - -// inula -const $tmpl = /*#__PURE__*/ $$template(`
Count value is .
`); +# Debug +See the compiled code in Vitest-UI on module graph tab (click the point of the file). +```shell +pnpm run test ``` - -命名规则 -对于dom的变量,前缀用`$`,后缀用`_number`,用dom类型作为变量名。待讨论 - -```jsx -const $tmpl = /*#__PURE__*/ $$template(`
Count value is .`); -const $div = $tmpl(), // 使用$div - $text = $div.firstChild, // 使用$text -``` - -**内部函数用`$$`作为前缀** - -### insert - -```jsx -const $tmpl = /*#__PURE__*/ $$template(`
Count value is .`); -const $div = $tmpl(), - $text = $div.firstChild, - $text_1 = $text.nextSibling, - $$insert -($div, count, $text_1); -return $div; -``` -## Class -### inline class -直接修改dom的`className` -```jsx - /** - * 源码: - * const Comp = () => { - * const color = 'red'; - * return
Count value is 0.
; - * } - */ - // 编译后: -const $tmpl = /*#__PURE__*/ $$template('
Count value is 0.'); -const Comp = () => { - const color = 'red'; - return (() => { - const _el$ = $tmpl(); - $$effect(() => (_el$.className = color)); - return _el$; - })(); -}; -render(() => $$runComponent(Comp, {}), container); -```` - -### Object class -通过`$$className`设置class -```jsx - /** - * 源码: - * const Comp = () => { - * return
Count value is 0.
; - * } - */ - // 编译后: - const $tmpl = /*#__PURE__*/ $$template('
Count value is 0.'); - const Comp = () => { - return (() => { - const _el$ = $tmpl(); - $$className(_el$, { - red: true, - }); - return _el$; - })(); - }; - render(() => $$runComponent(Comp, {}), container); -``` - -### Array class -通过`$$className`设置class -```jsx - /** - * 源码: - * const Comp = () => { - * return
Count value is 0.
; - * } - */ - // 编译后: - const $tmpl = /*#__PURE__*/ $$template('
Count value is 0.'); - const Comp = () => { - return (() => { - const _el$ = $tmpl(); - $$className(_el$, ['red', 'green']); - return _el$; - })(); - }; - render(() => $$runComponent(Comp, {}), container); -``` - -## Attribute -使用`$$setAttribute`设置属性 -```jsx -function App() { - return ( -

parallel

- ); -} - -// 编译后: -const $tmpl = /*#__PURE__*/ $$template('
Count value is 0.'); -const Comp = () => { - return (() => { - const $div = $tmpl(); - $$setAttribute($div, 'id', 'test'); - return $div; - })(); -}; -``` -使用响应式时,包裹在`$$effect`中 -```jsx - //源码: -function App() { - const id = reactive('el'); - return ( -

parallel

- ); -} - -// 编译后: -const $tmpl = /*#__PURE__*/ $$template('
Count value is 0.'); -const id = reactive('el'); -const Comp = () => { - return (() => { - const _el$ = $tmpl(); - $$effect(() => $$setAttribute(_el$, 'id', id.get())); - return _el$; - })(); -}; -``` - -## Fragment -Fragment编译为数组,并且每个元素都是一个IIFE,返回对应的dom。 -```jsx -/** - * 源码: - * const CountingComponent = () => { - * const [count, setCount] = createSignal(0); - * const add = () => { - * setCount((c) => c + 1); - * } - * return <> - *
Count value is {count()}.
- *
- * ; - * }; - */ - - // 编译后: -const $tmpl = /*#__PURE__*/ $$template('
Count value is .'), - $tmpl_2 = /*#__PURE__*/ $$template('
-// to -$$on(_el$6, 'click', handleDeleteClick, true); - - -// to -$$on(_el$6, 'change', handleDeleteClick, true); -``` - -## Dom Ref -```jsx -function App() { - let myDiv; - return
My Element
; -} - -// to - -function App() { - let myDiv; - return (() => { - const $div = _tmpl$(); - const $ref = myDiv; - typeof $ref === "function" ? $$bindRef($ref, _el$) : myDiv = $div; - return _el$; - })(); -} -``` -## Styling -### Inline Style -```jsx -/** - * 源码: - * const CountingComponent = () => { - * return
Count value is 0.
; - * }; - * - * render(() => , container); - */ - - // 编译后: -const $tmpl = /*#__PURE__*/ $$template('
Count value is 0.'); -const Comp = () => { - return $tmpl(); -}; -render(() => $$runComponent(Comp, {}), container); -``` -### Object Style -每个style属性通过`$$setProperty`设置。 -动态style通过`$$effect`包裹。 -```jsx - -/** - * 源码: - * const Comp = () => { - * const color = reactive('red'); - * return
Count value is 0.
; - * } - */ - - // 编译后: -const $tmpl = /*#__PURE__*/ $$template('
Count value is 0.'); -const Comp = () => { - return (() => { - const _el$ = $tmpl(); - $$effect(() => - color.get() != null ? _el$.style.setProperty('color', color.get()) : _el$.style.removeProperty('color') - ); - _el$.style.setProperty('display', 'flex'); - return _el$; - })(); -}; - -``` -## Loop - -```jsx - - {todo => <>} - - -// to -$$runComponent(For, { - get each() { - return state.todoList; - }, - children: todo => [ - $$runComponent(Todo, { - todo: todo, - }), - $$runComponent(Todo, { - todo: todo, - }), - ], -}) - -``` - -## Conditional -编译为`Cond`组件,`Cond`组件接收`branches`属性。 -`branches`属性为一个数组,数组中的每个元素都是一个数组,数组的第一个元素是条件,第二个元素是返回的dom。 -```ts -// It's boolean when the condition is static of default branch -// Otherwise it's a function that return boolean -type CondExpression = boolean | (() => boolean); -// When branch only include static JSXElement the branch can be a JSXElement -// Otherwise, the branch should be a function that return JSXElement -type Branch = JSXElement | (() => JSXElement); - -export interface CondProps { - // Array of tuples, first item is the condition, second is the branch to render - branches: [CondExpression, Branch][]; -} -``` -```jsx -/** - * 源码: - * const fn = vi.fn(); - * function App() { - * const x = reactive(7); - * - * return ( - *
- *

if

- * 10}> - *

{x.get()} is greater than 10

- *
- * x.get()}> - *

{x.get()} is less than 5

- *
- * - *

{x.get()} is between 5 and 10

- *
- *
- * ); - * } - * render(() => , container); - */ - - // 编译后: - -const _tmpl$ = /*#__PURE__*/ _$template(`

is greater than 10`), - _tmpl$2 = /*#__PURE__*/ _$template(`

is less than 5`), - _tmpl$3 = /*#__PURE__*/ _$template(`

xxx`), - _tmpl$4 = /*#__PURE__*/ _$template(`

is between 5 and 10`); -let change; - -function App() { - const x = reactive(7); - change = v => x.set(v); - return (() => { - const _el$ = _tmpl$3(), - _el$2 = _el$.firstChild; - _$insert( - _el$, - _$runComponent(Cond, { - get branches() { - return [ - [ - () => x.get() > 10, - () => { - const _el$3 = _tmpl$(), - _el$4 = _el$3.firstChild; - _$insert(_el$3, x, _el$4); - return _el$3; - }, - ], - [ - () => 5 > x.get(), - () => { - const _el$5 = _tmpl$2(), - _el$6 = _el$5.firstChild; - _$insert(_el$5, x, _el$6); - return _el$5; - }, - ], - [ - true, - () => { - const _el$7 = _tmpl$4(), - _el$8 = _el$7.firstChild; - _$insert(_el$7, x, _el$8); - return _el$7; - }, - ], - ]; - }, - }), - null - ); - - return _el$; - })(); -} - -render(() => _$runComponent(App, {}), container); -``` -## Early Return - -# **Component composition** - -## Props - -使用`.get()`的表达式作为props时,转为对象时通过getter包装。 -使用props时,通过props.(参数名)访问来保证在组件时读取props时能够保持响应。 - -> 好处: 1. 使用props时无需感知是否为响应式,无需额外判断 2. 使用统一,JSX和函数体JS都通过.get() 读值 -> 问题: 1. 不能解构 -> 通过2.0 API编译语法糖解决 - -```jsx -function App() { - const name = reactive("init") - return -} - -function Button({name}) { - cosnt - greeting = name + '!' - return

{greeting}

-} - -// 2.0 语法编译后 -function App() { - const name = reactive('init'); - return -} - -function Button(props) { - cosnt - greeting = computed(() => props.name + '!') - return

{greeting.get()}

-} -``` - -备选方案: -JSX中传递响应式时直接传递响应式变量,无需`.get()`。 -使用时从props取对应的响应式值进行使用。 - -> 好处: 可以解构,组件显式处理props中的响应式 -> 问题: 1. 使用props时需额外判断props是否为响应式,2.0API 无法向下编译 - -2. JSX使用响应式不要get,JS使用要get - -```jsx -function App() { - const name = reactive('init'); - return -} - -$$runComponent(Button, { - class: name -} -}) - -function Button({name}) { - cosnt - greeting = computed(() => (isReactiveObj(props.name) ? props.name.get() : props.name) + '!') - return

{reeting.get()}

-} -``` - -## Slot -通过children属性传递slot。IIFE包裹slot,返回对应的dom。 -```jsx -/** - * 源码: - * const CountValue = (props) => { - * return
Title: {props.children}
; - * } - * - * const CountingComponent = () => { - * const [count, setCount] = createSignal(0); - * - * return
- *

FOO

- *
; - * }; - * - * render(() => , document.getElementById("app")); - */ - - // 编译后: -const _tmpl$ = /*#__PURE__*/ $$template(`
Title: `), - _tmpl$2 = /*#__PURE__*/ $$template(`

Your count is .`), - _tmpl$3 = /*#__PURE__*/ $$template(`
`); -const CountValue = props => { - return (() => { - const _el$ = _tmpl$(), - _el$2 = _el$.firstChild; - $$insert(_el$, () => props.children, null); - return _el$; - })(); -}; -const CountingComponent = () => { - const count = reactive(0); - const add = () => { - count.set(c => c + 1); - }; - return (() => { - const _el$3 = _tmpl$3(), - _el$8 = _el$3.firstChild, - _el$9 = _el$8.firstChild; - $$insert( - _el$3, - $$runComponent(CountValue, { - get children() { - const _el$4 = _tmpl$2(), - _el$5 = _el$4.firstChild, - _el$7 = _el$5.nextSibling, - _el$6 = _el$7.nextSibling; - $$insert(_el$4, count, _el$7); - return _el$4; - }, - }), - _el$8, - ); - return _el$3; - })(); -}; -``` -## Context +detail: https://github.com/vitest-dev/vitest/discussions/2242 diff --git a/packages/inula-novdom/babel.config.js b/packages/inula-novdom/babel.config.js deleted file mode 100644 index 9fd8d3d1..00000000 --- a/packages/inula-novdom/babel.config.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2023 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. - */ - -module.exports = { - presets: [ - [ - '@babel/preset-env', - ], - [ - '@babel/preset-typescript', - ] - ] -}; diff --git a/packages/inula-novdom/src/dom.ts b/packages/inula-novdom/src/dom.ts index 41af470f..27153578 100644 --- a/packages/inula-novdom/src/dom.ts +++ b/packages/inula-novdom/src/dom.ts @@ -385,3 +385,7 @@ export function bindRef(node: T, ref: RefObject | RefCallback): void { throw new Error('Invalid ref'); } } + +export function createElement(tag: string) { + return document.createElement(tag); +} diff --git a/packages/inula-novdom/tests/render.test.tsx b/packages/inula-novdom/tests/render.test.tsx index 89509b80..c3420b20 100644 --- a/packages/inula-novdom/tests/render.test.tsx +++ b/packages/inula-novdom/tests/render.test.tsx @@ -22,6 +22,7 @@ import { style as $$style, className as $$className, setAttribute as $$attr, + createElement } from '../src/dom'; import { runComponent as $$runComponent, render } from '../src/core'; import { delegateEvents as $$delegateEvents, addEventListener as $$on } from '../src/event'; @@ -40,9 +41,8 @@ describe('render', () => { */ // 编译后: - const $tmpl = /*#__PURE__*/ $$template('
Count value is 0.'); const CountingComponent = () => { - return $tmpl(); + return
Count value is 0.
; }; render(() => $$runComponent(CountingComponent, {}), container); @@ -443,9 +443,7 @@ describe('render', () => { const color = reactive('red'); return (() => { const _el$ = $tmpl(); - $$effect(() => - $$style(_el$, { color: color.get() }) - ); + $$effect(() => $$style(_el$, { color: color.get() })); return _el$; })(); }; diff --git a/packages/inula-novdom/vitest.config.ts b/packages/inula-novdom/vitest.config.ts index c3ae41c7..20abc8ae 100644 --- a/packages/inula-novdom/vitest.config.ts +++ b/packages/inula-novdom/vitest.config.ts @@ -15,8 +15,19 @@ // vitest.config.ts import { defineConfig } from 'vitest/config'; +import inula from 'vite-plugin-inula-no-vdom'; export default defineConfig({ + esbuild: { + jsx: 'preserve', + }, + resolve: { + conditions: ['dev'], + }, + plugins: [ + // @ts-expect-error TODO: fix vite plugin interface is not compatible + inula(), + ], test: { environment: 'jsdom', // or 'jsdom', 'node' }, diff --git a/packages/transpiler/babel-preset-inula-jsx/src/pluginProvider.ts b/packages/transpiler/babel-preset-inula-jsx/src/pluginProvider.ts index 5ccd8f32..0c5d80a8 100644 --- a/packages/transpiler/babel-preset-inula-jsx/src/pluginProvider.ts +++ b/packages/transpiler/babel-preset-inula-jsx/src/pluginProvider.ts @@ -8,7 +8,7 @@ import { attributeMap, htmlTags, importMap } from './const'; export class PluginProvider { - private static readonly inulaPackageName = 'inula'; + private static readonly inulaPackageName = 'inula-reactive'; // ---- Plugin Level ---- private readonly babelApi: typeof babel private readonly t: typeof t diff --git a/packages/transpiler/jsx-view-generator/src/test/exp.test.ts b/packages/transpiler/jsx-view-generator/src/test/exp.test.ts index d9585f3c..f424392c 100644 --- a/packages/transpiler/jsx-view-generator/src/test/exp.test.ts +++ b/packages/transpiler/jsx-view-generator/src/test/exp.test.ts @@ -1,14 +1,59 @@ import { describe, it } from 'vitest'; import { expectView } from './mock'; - describe('Expression', () => { it('should generate a expression Node', () => { - expectView(/*jsx*/` + expectView( + /*jsx*/ ` <>{expr} - `, /*js*/` + `, + /*js*/ ` const $node0 = expr - `); + ` + ); + }); + + it('should generate a expression Node in middle of text', () => { + expectView( + /*jsx*/ ` +
111{expr}222
+ `, + /*js*/ ` + const $node0 = createElement("div"); + const $node1 = createText("111"); + insert($node0, $node1); + const $node2 = expr; + insert($node0, $node2); + const $node3 = createText("222"); + insert($node0, $node3); + ` + ); + }); + + it('should generate a expression Node in middle of text in template', () => { + expectView( + /*jsx*/ ` +
111{expr}222
+ `, + /*js*/ ` + const $node0 = $template0.cloneNode(true); + const $node1 = $node0.firstChild; + const $node2 = $node1.firstChild.nextSibling; + const $node3 = expr; + insert($node1, $node3, $node2); + ` + , [ + `const $template0 = () => { + const $node0 = createElement("div"); + const $node1 = createElement("div"); + const $node2 = createText("111"); + insert($node1, $node2); + const $node3 = createText("222"); + insert($node1, $node3); + insert($node0, $node1); + return $node0; + };` + ] + ); }); }); - \ No newline at end of file diff --git a/packages/transpiler/vite-plugin-inula-no-vdom/package.json b/packages/transpiler/vite-plugin-inula-no-vdom/package.json new file mode 100644 index 00000000..01602723 --- /dev/null +++ b/packages/transpiler/vite-plugin-inula-no-vdom/package.json @@ -0,0 +1,37 @@ +{ + "name": "vite-plugin-inula-no-vdom", + "version": "1.0.0", + "description": "", + "scripts": { + "build": "tsup --sourcemap" + }, + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "typings": "dist/index.d.ts", + "author": "", + "license": "ISC", + "peerDependencies": { + "vite": "^5.0.0" + }, + "dependencies": { + "@babel/core": "^7.23.9", + "@types/babel__core": "^7.20.5", + "babel-preset-inula-jsx": "workspace:*", + "@rollup/plugin-babel": "^5.3.0", + "minimatch": "^9.0.3", + "tsup": "^6.7.0", + "vite": "^5.0.0" + }, + "tsup": { + "entry": [ + "src/index.ts" + ], + "format": [ + "cjs", + "esm" + ], + "clean": true, + "dts": true + } +} diff --git a/packages/transpiler/vite-plugin-inula-no-vdom/src/index.ts b/packages/transpiler/vite-plugin-inula-no-vdom/src/index.ts new file mode 100644 index 00000000..c5eef4d4 --- /dev/null +++ b/packages/transpiler/vite-plugin-inula-no-vdom/src/index.ts @@ -0,0 +1,64 @@ +/* + * 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 * as babel from '@babel/core'; +import inula, { InulaOption } from 'babel-preset-inula-jsx'; +import { Plugin, TransformResult } from 'vite'; +import { minimatch } from 'minimatch'; + +function toArray(arr: T | T[]): T[] { + return Array.isArray(arr) ? arr : [arr]; +} + +/** + * Check if the transformation should be applied + * @param id the file path + * @param files the files to apply the transformation + * @param excludeFiles the files to exclude the transformation + */ +function shouldApplyTransformation(id: string, files: string | string[], excludeFiles: string | string[]) { + let enter = false; + for (const allowedPath of toArray(files)) { + if (minimatch(id, allowedPath)) { + enter = true; + break; + } + } + for (const notAllowedPath of toArray(excludeFiles)) { + if (minimatch(id, notAllowedPath)) { + enter = false; + break; + } + } + return enter; +} + +export default function inulaPugin(options: InulaOption = {}): Plugin { + const { files = '**/*.{js,jsx,ts,tsx}', excludeFiles = '**/{dist,node_modules,lib}/*.{js,ts}' } = options; + return { + name: 'inula-no-vdom', + enforce: 'pre', + transform(source, id) { + if (!shouldApplyTransformation(id, files, excludeFiles)) return; + + return babel.transform(source, { + babelrc: false, + configFile: false, + filename: id, + sourceMaps: true, + presets: [[inula, options]], + }) as TransformResult; + }, + }; +} diff --git a/packages/transpiler/vite-plugin-inula-no-vdom/tsconfig.json b/packages/transpiler/vite-plugin-inula-no-vdom/tsconfig.json new file mode 100644 index 00000000..e0932d78 --- /dev/null +++ b/packages/transpiler/vite-plugin-inula-no-vdom/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true + }, + "ts-node": { + "esm": true + } +} \ No newline at end of file