From 2445208856574e179b3e1ba1fe875ce0dc7da104 Mon Sep 17 00:00:00 2001 From: Hoikan <408255371@qq.com> Date: Fri, 23 Feb 2024 02:19:37 +0000 Subject: [PATCH] !159 fix(transpiler): auto import * fix(transpiler): auto import * feat(transpiler): auto import --- packages/inula-novdom/package.json | 4 +- packages/inula-novdom/src/components/index.ts | 19 +++ packages/inula-novdom/src/dom.ts | 1 + packages/inula-novdom/src/index.ts | 21 +++ packages/inula-novdom/tests/Dynamic.test.tsx | 123 +++--------------- packages/inula-novdom/tsconfig.json | 20 +++ packages/inula-novdom/vitest.config.ts | 6 +- .../babel-preset-inula-jsx/src/const.ts | 18 +-- .../src/pluginProvider.ts | 22 +++- .../src/HelperGenerators/BaseGenerator.ts | 86 +++++++----- .../src/HelperGenerators/HTMLPropGenerator.ts | 6 + .../src/NodeGenerators/CompGenerator.ts | 10 +- .../src/NodeGenerators/HTMLGenerator.ts | 10 +- .../src/NodeGenerators/TemplateGenerator.ts | 5 +- .../src/NodeGenerators/TextGenerator.ts | 3 +- .../jsx-view-generator/src/generate.ts | 54 +++++--- .../jsx-view-generator/src/test/comp.test.ts | 85 +++++++----- .../jsx-view-generator/src/test/const.ts | 2 +- .../jsx-view-generator/src/test/exp.test.ts | 10 +- .../jsx-view-generator/src/test/html.test.ts | 7 +- .../jsx-view-generator/src/test/mock.ts | 12 +- .../src/test/template.test.ts | 8 +- .../jsx-view-generator/src/types.ts | 7 +- .../transpiler/jsx-view-parser/src/parser.ts | 22 ++-- 24 files changed, 314 insertions(+), 247 deletions(-) create mode 100644 packages/inula-novdom/src/components/index.ts create mode 100644 packages/inula-novdom/src/index.ts create mode 100644 packages/inula-novdom/tsconfig.json diff --git a/packages/inula-novdom/package.json b/packages/inula-novdom/package.json index 0ef2f887..9f01182c 100644 --- a/packages/inula-novdom/package.json +++ b/packages/inula-novdom/package.json @@ -1,8 +1,8 @@ { - "name": "inula-novdom", + "name": "@inula/no-vdom", "version": "0.0.1", "description": "no vdom runtime", - "main": "index.js", + "main": "./src/index.ts", "scripts": { "test": "vitest --ui", "bench": "vitest bench", diff --git a/packages/inula-novdom/src/components/index.ts b/packages/inula-novdom/src/components/index.ts new file mode 100644 index 00000000..2b536324 --- /dev/null +++ b/packages/inula-novdom/src/components/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export * from './Cond'; +export * from './Dynamic'; +export * from './Env'; +export * from './For'; diff --git a/packages/inula-novdom/src/dom.ts b/packages/inula-novdom/src/dom.ts index c305c73a..e2bb8456 100644 --- a/packages/inula-novdom/src/dom.ts +++ b/packages/inula-novdom/src/dom.ts @@ -61,6 +61,7 @@ function watchRender(fn: (value: any) => any, initial?: any): void { } function insertExpression(parent: Node, value: any, prevValue: any, marker?: Node): any { + Array.isArray(value) && value.length === 1 && (value = value[0]); let result: any; while (typeof prevValue === 'function') { prevValue = prevValue(); diff --git a/packages/inula-novdom/src/index.ts b/packages/inula-novdom/src/index.ts new file mode 100644 index 00000000..9bc1201b --- /dev/null +++ b/packages/inula-novdom/src/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export * from 'inula-reactive'; +export * from './core'; +export * from './dom'; +export * from './event'; +export * from './components'; +export * from './type'; diff --git a/packages/inula-novdom/tests/Dynamic.test.tsx b/packages/inula-novdom/tests/Dynamic.test.tsx index 9fc6f5c6..e22cd27e 100644 --- a/packages/inula-novdom/tests/Dynamic.test.tsx +++ b/packages/inula-novdom/tests/Dynamic.test.tsx @@ -17,30 +17,15 @@ import { describe, expect } from 'vitest'; import { domTest as it } from './utils'; -import { template as $$template, insert as $$insert } from '../src/dom'; -import { runComponent as $$runComponent, render } from '../src/core'; -import { Dynamic } from '../src/components/Dynamic'; -import { reactive } from 'inula-reactive'; +import { render, Dynamic, reactive } from '@inula/no-vdom'; describe('Dynamic', () => { it('should work with native elements.', ({ container }) => { - /** - * 源码: - * function App() { - * return foo; - * } - * render(() => , container); - */ - - // 编译后: function App() { - return $$runComponent(Dynamic, { - component: 'h1', - children: 'foo', - }); + return foo; } - render(() => $$runComponent(App, {}), container); + render(() => , container); expect(container).toMatchInlineSnapshot(`

@@ -51,36 +36,15 @@ describe('Dynamic', () => { }); it('should work with components.', ({ container }) => { - /** - * 源码: - * function App() { - * return ; - * } - * function Title(props) { - * return

{props.name}

; - * } - * render(() => , container); - */ - - // 编译后: function App() { - return $$runComponent(Dynamic, { - component: Title, - name: 'bar', - }); + return ; } - const _tmpl = /*#__PURE__*/ $$template('

'); - function Title(props) { - return (() => { - const _el$ = _tmpl(); - $$insert(_el$, () => props.name, null); - return _el$; - })(); + return

{props.name}

; } - render(() => $$runComponent(App, {}), container); + render(() => , container); expect(container).toMatchInlineSnapshot(`

@@ -91,62 +55,16 @@ describe('Dynamic', () => { }); it('should throw on invalid component.', ({ container }) => { - /** - * 源码: - * function App() { - * return ; - * } - * render(() => , container); - */ - - // 编译后: function App() { - return $$runComponent(Dynamic, { - component: null, - }); + return ; } - expect(() => render(() => $$runComponent(App, {}), container)).toThrowError('Invalid component for Dynamic'); + expect(() => render(() => , container)).toThrowError('Invalid component for Dynamic'); }); it('should change component.', async ({ container }) => { - /** - * 源码: - * const H1 = (props) =>

{props.children}

; - * const H3 = (props) =>

{props.children}

; - * function App() { - * const comp = reactive('h1'); - * const comps = { - * H1, - * H3, - * h1: 'h1', - * h2: 'h2', - * } - * return ( - *
- * foo - *
- * ); - * } - * render(() => , container); - * - */ - - // 编译后: - const _tmpl$ = /*#__PURE__*/ $$template('
'); - const _h1 = /*#__PURE__*/ $$template('

'), - _h3 = /*#__PURE__*/ $$template('

'); - const H1 = (props: { children: any }) => { - const _el$ = _h1(); - $$insert(_el$, () => props.children); - return _el$; - }; - const H3 = (props: { children: any }) => { - const _el$3 = _h3(); - $$insert(_el$3, () => props.children); - return _el$3; - }; - + const H1 = props =>

{props.children}

; + const H3 = props =>

{props.children}

; const comp = reactive('h1'); function App() { @@ -156,22 +74,15 @@ describe('Dynamic', () => { h1: 'h1', h2: 'h2', }; - return (() => { - const _div = _tmpl$(); - $$insert( - _div, - $$runComponent(Dynamic, { - get component() { - return comps[comp.get()]; - }, - children: 'foo', - }) - ); - return _div; - })(); + return ( +
+ foo +
+ ); } - render(() => $$runComponent(App, {}), container); + render(() => , container); + expect(container.innerHTML).toMatchInlineSnapshot('"

foo

"'); comp.set('h2'); expect(container.innerHTML).toMatchInlineSnapshot('"

foo

"'); diff --git a/packages/inula-novdom/tsconfig.json b/packages/inula-novdom/tsconfig.json new file mode 100644 index 00000000..700defdb --- /dev/null +++ b/packages/inula-novdom/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": [ + "ESNext", + "DOM" + ], + "moduleResolution": "Node", + "paths": { + "@inula/no-vdom": [ + "./src/index.ts" + ] + }, + "esModuleInterop": true + }, + "ts-node": { + "esm": true + } +} diff --git a/packages/inula-novdom/vitest.config.ts b/packages/inula-novdom/vitest.config.ts index 417cc23e..07892b9e 100644 --- a/packages/inula-novdom/vitest.config.ts +++ b/packages/inula-novdom/vitest.config.ts @@ -16,17 +16,21 @@ // vitest.config.ts import { defineConfig } from 'vitest/config'; import inula from 'vite-plugin-inula-no-vdom'; +import * as path from 'node:path'; export default defineConfig({ esbuild: { jsx: 'preserve', }, resolve: { + alias: { + '@inula/no-vdom': path.resolve(__dirname, 'src'), + }, conditions: ['dev'], }, plugins: [ // @ts-expect-error TODO: fix vite plugin interface is not compatible - inula({ packageName: 'inula-reactive' }), + inula(), ], test: { environment: 'jsdom', // or 'jsdom', 'node' diff --git a/packages/transpiler/babel-preset-inula-jsx/src/const.ts b/packages/transpiler/babel-preset-inula-jsx/src/const.ts index 1383b408..34744cc8 100644 --- a/packages/transpiler/babel-preset-inula-jsx/src/const.ts +++ b/packages/transpiler/babel-preset-inula-jsx/src/const.ts @@ -1,16 +1,16 @@ export const importMap = [ - 'createElement', - 'setStyle', - 'setAttribute', - 'setDataset', - 'setProperty', - 'setEvent', - 'delegateEvent', + 'createElement', + 'setStyle', + 'setAttribute', + 'setDataset', + 'setProperty', + 'setEvent', + 'delegateEvent', 'addEventListener', 'watch', 'insert', - 'createComponent', + 'runComponent', 'createText' ].reduce>((acc, cur) => { acc[cur] = cur; @@ -510,4 +510,4 @@ export const attributeMap = { ariaRowIndex: ['*'], ariaRowSpan: ['*'], ariaSetSize: ['*'], -}; \ No newline at end of file +}; diff --git a/packages/transpiler/babel-preset-inula-jsx/src/pluginProvider.ts b/packages/transpiler/babel-preset-inula-jsx/src/pluginProvider.ts index 30b55295..4acc0ab5 100644 --- a/packages/transpiler/babel-preset-inula-jsx/src/pluginProvider.ts +++ b/packages/transpiler/babel-preset-inula-jsx/src/pluginProvider.ts @@ -6,9 +6,8 @@ import { generateView } from '@inula/jsx-view-generator'; import { parseView } from '@inula/jsx-view-parser'; import { attributeMap, htmlTags, importMap } from './const'; - export class PluginProvider { - private inulaPackageName = 'inula-reactive'; + private inulaPackageName = '@inula/no-vdom'; // ---- Plugin Level ---- private readonly babelApi: typeof babel private readonly t: typeof t @@ -47,6 +46,9 @@ export class PluginProvider { private templateIdx = -1 + // ---- record used apis for automatic import + private allUsedApis: Set = new Set(); + programEnterVisitor( path: NodePath, filename: string | undefined @@ -71,8 +73,19 @@ export class PluginProvider { programExitVisitor(path: NodePath): void { if (!this.fileEnter) return; this.fileEnter = false; + if (this.allUsedApis.size) { + this.programNode!.body.unshift(this.autoImport()); + } } + autoImport(): t.ImportDeclaration { + const t = this.babelApi.types; + // add automatic import + return t.importDeclaration( + [...this.allUsedApis].map(api => t.importSpecifier(t.identifier(api), t.identifier(api))), + t.stringLiteral(this.inulaPackageName), + ); + } jsxElementVisitor(path: NodePath): void { if (!this.fileEnter) return; @@ -81,7 +94,7 @@ export class PluginProvider { htmlTags: this.htmlTags, parseTemplate: this.parseTemplate, }); - const [templates, viewAst] = generateView(viewUnits, { + const [templates, viewAst, usedApis] = generateView(viewUnits, { babelApi: this.babelApi, importMap, attributeMap: this.attributeMap, @@ -89,7 +102,8 @@ export class PluginProvider { this.templateIdx += templates.length; // ---- Add templates to the program this.programNode!.body.unshift(...templates); - + // ---- collect the used apis + usedApis.forEach(api => this.allUsedApis.add(api)) // ---- Replace the JSXElement with the viewAst path.replaceWith(viewAst); path.skip(); diff --git a/packages/transpiler/jsx-view-generator/src/HelperGenerators/BaseGenerator.ts b/packages/transpiler/jsx-view-generator/src/HelperGenerators/BaseGenerator.ts index 81c960fe..afe21aef 100644 --- a/packages/transpiler/jsx-view-generator/src/HelperGenerators/BaseGenerator.ts +++ b/packages/transpiler/jsx-view-generator/src/HelperGenerators/BaseGenerator.ts @@ -1,67 +1,71 @@ import { UnitProp, ViewUnit } from '@inula/jsx-view-parser'; -import { ViewGeneratorConfig } from '../types'; +import { ViewGeneratorConfig, ViewGeneratorContext } from '../types'; import type { types as t, traverse } from '@babel/core'; - export default class BaseGenerator { - readonly viewUnit: ViewUnit - readonly config: ViewGeneratorConfig - readonly t - readonly traverse: typeof traverse - readonly elementAttributeMap - readonly importMap + readonly viewUnit: ViewUnit; + readonly config: ViewGeneratorConfig; + readonly t; + readonly traverse: typeof traverse; + readonly elementAttributeMap; + readonly importMap; + readonly context : ViewGeneratorContext; - constructor(viewUnit: ViewUnit, config: ViewGeneratorConfig) { + constructor(viewUnit: ViewUnit, config: ViewGeneratorConfig, context: ViewGeneratorContext) { this.config = config; this.t = config.babelApi.types; this.traverse = config.babelApi.traverse; this.importMap = config.importMap; this.viewUnit = viewUnit; this.elementAttributeMap = config.attributeMap - ? Object.entries(config.attributeMap).reduce>( - (acc, [key, elements]) => { - elements.forEach(element => { - if (!acc[element]) acc[element] = []; - acc[element].push(key); - }); - return acc; - }, - {} - ) + ? Object.entries(config.attributeMap).reduce>((acc, [key, elements]) => { + elements.forEach(element => { + if (!acc[element]) acc[element] = []; + acc[element].push(key); + }); + return acc; + }, {}) : {}; + this.context = context; } // ---- Init Statements - private readonly initStatements: t.Statement[] = [] + private readonly initStatements: t.Statement[] = []; + addStatement(...statements: t.Statement[]) { this.initStatements.push(...statements); } - private readonly templates: t.Statement[] = [] + private readonly templates: t.Statement[] = []; + addTemplate(...template: t.Statement[]) { this.templates.push(...template); } + addUsedApi(apiName: string) { + this.context.collectApis(apiName); + } + // ---- Generate ---- /** * @brief To be implemented by the subclass */ run(): string { - return ''; + return ''; } - generate(): [string, t.Statement[], t.Statement[]]{ + generate(): [string, t.Statement[], t.Statement[]] { const nodeName = this.run(); return [nodeName, this.initStatements, this.templates]; } - + // ---- Reactivity ---- /** * @brief Check if the expression is reactive, which satisfies the following conditions: * 1. Contains .get() property * 2. The whole expression is not a function - * @param expression - * @returns + * @param expression + * @returns */ checkReactive(expression: t.Expression) { if (this.t.isFunction(expression)) return false; @@ -72,7 +76,7 @@ export default class BaseGenerator { reactive = true; path.stop(); } - } + }, }); return reactive; } @@ -81,18 +85,20 @@ export default class BaseGenerator { private readonly prefixMap = { node: '$node', template: '$template', - } + }; nodeIdx = -1; + geneNodeName(idx?: number): string { return `${this.prefixMap.node}${idx ?? ++this.nodeIdx}`; } + templateIdx = -1; + generateTemplateName() { return `${this.prefixMap.template}${++this.templateIdx}`; } - // ---- Utils ---- /** * @brief Wrap the value in a file @@ -110,8 +116,9 @@ export default class BaseGenerator { ); } - createCollector(): [t.Statement[], (statement: t.Statement | t.Statement[] | null) => void]{ + createCollector(): [t.Statement[], (statement: t.Statement | t.Statement[] | null) => void] { const statements: t.Statement[] = []; + function collect(statement: t.Statement | t.Statement[] | null) { if (Array.isArray(statement)) { statements.push(...statement); @@ -119,10 +126,18 @@ export default class BaseGenerator { statements.push(statement); } } + return [statements, collect]; } - parseViewProp(prop: UnitProp, generateView: (units: ViewUnit[], config: ViewGeneratorConfig, templateIdx: number) => [t.Statement[], t.ExpressionStatement]): t.Expression { + parseViewProp( + prop: UnitProp, + generateView: ( + units: ViewUnit[], + config: ViewGeneratorConfig, + templateIdx: number + ) => [t.Statement[], t.ExpressionStatement, string[]] + ): t.Expression { let value = prop.value; const viewPropMap = prop.viewPropMap; const propNodeMap = Object.fromEntries( @@ -142,13 +157,16 @@ export default class BaseGenerator { } path.replaceWith(propNodeMap[key]); } - } + }, }); - + return value; } - parseProps(props: Record, generateView: (units: ViewUnit[], config: ViewGeneratorConfig) => [t.Statement[], t.ExpressionStatement]) { + parseProps( + props: Record, + generateView: (units: ViewUnit[], config: ViewGeneratorConfig) => [t.Statement[], t.ExpressionStatement, string[]] + ) { return Object.fromEntries( Object.entries(props).map(([key, prop]) => { return [key, this.parseViewProp(prop, generateView)]; diff --git a/packages/transpiler/jsx-view-generator/src/HelperGenerators/HTMLPropGenerator.ts b/packages/transpiler/jsx-view-generator/src/HelperGenerators/HTMLPropGenerator.ts index e4189df0..2ed4dde6 100644 --- a/packages/transpiler/jsx-view-generator/src/HelperGenerators/HTMLPropGenerator.ts +++ b/packages/transpiler/jsx-view-generator/src/HelperGenerators/HTMLPropGenerator.ts @@ -48,6 +48,7 @@ export class HTMLPropGenerator extends BaseGenerator { nodeName: string, value: t.Expression, ) { + this.addUsedApi(this.importMap.setStyle); return this.t.callExpression( this.t.identifier(this.importMap.setStyle), [this.t.identifier(nodeName), value] @@ -62,6 +63,7 @@ export class HTMLPropGenerator extends BaseGenerator { nodeName: string, value: t.Expression, ) { + this.addUsedApi(this.importMap.setDataset); return this.t.callExpression( this.t.identifier(this.importMap.setDataset), [this.t.identifier(nodeName), value] @@ -96,6 +98,7 @@ export class HTMLPropGenerator extends BaseGenerator { key: string, value: t.Expression, ) { + this.addUsedApi(this.importMap.setProperty); return this.t.callExpression( this.t.identifier(this.importMap.setProperty), [this.t.identifier(nodeName), this.t.stringLiteral(key), value] @@ -130,6 +133,7 @@ export class HTMLPropGenerator extends BaseGenerator { key: string, value: t.Expression, ) { + this.addUsedApi(this.importMap.setAttribute); return this.t.callExpression( this.t.identifier(this.importMap.setAttribute), [this.t.identifier(nodeName), this.t.stringLiteral(key), value] @@ -145,6 +149,7 @@ export class HTMLPropGenerator extends BaseGenerator { eventName: string, value: t.Expression, ) { + this.addUsedApi(this.importMap.delegateEvent); return this.t.callExpression( this.t.identifier(this.importMap.delegateEvent), [this.t.identifier(nodeName), this.t.stringLiteral(eventName), value] @@ -178,6 +183,7 @@ export class HTMLPropGenerator extends BaseGenerator { eventName: string, value: t.Expression, ) { + this.addUsedApi(this.importMap.addEventListener); return this.t.callExpression( this.t.identifier(this.importMap.addEventListener), [this.t.identifier(nodeName), this.t.stringLiteral(eventName), value] diff --git a/packages/transpiler/jsx-view-generator/src/NodeGenerators/CompGenerator.ts b/packages/transpiler/jsx-view-generator/src/NodeGenerators/CompGenerator.ts index e582b905..c6bd7a5b 100644 --- a/packages/transpiler/jsx-view-generator/src/NodeGenerators/CompGenerator.ts +++ b/packages/transpiler/jsx-view-generator/src/NodeGenerators/CompGenerator.ts @@ -16,7 +16,7 @@ export class CompGenerator extends BaseGenerator { /** * @View - * const $el = createComponent(tag, { + * const $el = runComponent(tag, { * ...props * }, spreadProps) */ @@ -47,7 +47,7 @@ export class CompGenerator extends BaseGenerator { ); })); if (children.length > 0) { - const statement = generateBlock(children, this.config); + const statement = generateBlock(children, this.config, this.context); propNode.properties.push( this.t.objectMethod('get', this.t.identifier('children'), [], statement) ); @@ -55,15 +55,15 @@ export class CompGenerator extends BaseGenerator { nodes.push(propNode); } - + this.addUsedApi(this.importMap.runComponent); return [name, this.t.variableDeclaration('const', [ this.t.variableDeclarator( this.t.identifier(name), this.t.callExpression( - this.t.identifier(this.importMap.createComponent), + this.t.identifier(this.importMap.runComponent), [tag, ...nodes] ) ) ])]; } -} \ No newline at end of file +} diff --git a/packages/transpiler/jsx-view-generator/src/NodeGenerators/HTMLGenerator.ts b/packages/transpiler/jsx-view-generator/src/NodeGenerators/HTMLGenerator.ts index f6124e6a..df83c2f7 100644 --- a/packages/transpiler/jsx-view-generator/src/NodeGenerators/HTMLGenerator.ts +++ b/packages/transpiler/jsx-view-generator/src/NodeGenerators/HTMLGenerator.ts @@ -29,16 +29,17 @@ export class HTMLGenerator extends HTMLPropGenerator { } /** - * @View + * @View * const $el = createElement(tag) */ declareHTMLNode(tag: t.Expression): [string, t.Statement] { const name = this.geneNodeName(); + this.addUsedApi(this.importMap.createElement); return [name, this.t.variableDeclaration('const', [ this.t.variableDeclarator( this.t.identifier(name), this.t.callExpression( - this.t.identifier('createElement'), + this.t.identifier(this.importMap.createElement), [tag] ) ) @@ -50,9 +51,10 @@ export class HTMLGenerator extends HTMLPropGenerator { * $insert($el, childNode) */ private insertChildNode( - parent: string, + parent: string, child: string ) { + this.addUsedApi(this.importMap.insert); return this.t.expressionStatement( this.t.callExpression( this.t.identifier(this.importMap.insert), @@ -60,4 +62,4 @@ export class HTMLGenerator extends HTMLPropGenerator { ) ); } -} \ No newline at end of file +} diff --git a/packages/transpiler/jsx-view-generator/src/NodeGenerators/TemplateGenerator.ts b/packages/transpiler/jsx-view-generator/src/NodeGenerators/TemplateGenerator.ts index db84dbdc..597cb3bf 100644 --- a/packages/transpiler/jsx-view-generator/src/NodeGenerators/TemplateGenerator.ts +++ b/packages/transpiler/jsx-view-generator/src/NodeGenerators/TemplateGenerator.ts @@ -110,11 +110,12 @@ export class TemplateGenerator extends HTMLPropGenerator{ * $insert($el, childNode) */ private insertChildNode( - parent: string, + parent: string, child: string, nextName: string ) { const nextNode = nextName ? [this.t.identifier(nextName)] : []; + this.addUsedApi(this.importMap.insert); return this.t.expressionStatement( this.t.callExpression( this.t.identifier(this.importMap.insert), @@ -319,4 +320,4 @@ export class TemplateGenerator extends HTMLPropGenerator{ } return [bestMatchName, path.slice(bestMatchCount), 0]; } -} \ No newline at end of file +} diff --git a/packages/transpiler/jsx-view-generator/src/NodeGenerators/TextGenerator.ts b/packages/transpiler/jsx-view-generator/src/NodeGenerators/TextGenerator.ts index 4abe02b2..c155becb 100644 --- a/packages/transpiler/jsx-view-generator/src/NodeGenerators/TextGenerator.ts +++ b/packages/transpiler/jsx-view-generator/src/NodeGenerators/TextGenerator.ts @@ -12,6 +12,7 @@ export class TextGenerator extends BaseGenerator { declareTextNode(content: t.Literal): [string, t.Statement] { const name = this.geneNodeName(); + this.addUsedApi(this.importMap.createText); return [name, this.t.variableDeclaration('const', [ this.t.variableDeclarator( this.t.identifier(name), @@ -22,4 +23,4 @@ export class TextGenerator extends BaseGenerator { ) ])]; } -} \ No newline at end of file +} diff --git a/packages/transpiler/jsx-view-generator/src/generate.ts b/packages/transpiler/jsx-view-generator/src/generate.ts index 301409df..30ab0c64 100644 --- a/packages/transpiler/jsx-view-generator/src/generate.ts +++ b/packages/transpiler/jsx-view-generator/src/generate.ts @@ -1,7 +1,7 @@ import { ViewUnit } from '@inula/jsx-view-parser'; import { CompGenerator } from './NodeGenerators/CompGenerator'; import { HTMLGenerator } from './NodeGenerators/HTMLGenerator'; -import { ViewGeneratorConfig } from './types'; +import { ViewGeneratorConfig, ViewGeneratorContext } from './types'; import type { types as t } from '@babel/core'; import { TemplateGenerator } from './NodeGenerators/TemplateGenerator'; import { TextGenerator } from './NodeGenerators/TextGenerator'; @@ -17,38 +17,54 @@ export const viewGeneratorMap = { env: CompGenerator, } as const; - -export function generateNew(oldGenerator: any, viewUnit: ViewUnit, resetIdx = true): [string, t.Statement[], t.Statement[]]{ - const generator = new viewGeneratorMap[viewUnit.type](viewUnit, oldGenerator.config); +export function generateNew( + oldGenerator: any, + viewUnit: ViewUnit, + resetIdx = true +): [string, t.Statement[], t.Statement[]] { + const generator = new viewGeneratorMap[viewUnit.type](viewUnit, oldGenerator.config, oldGenerator.context); if (resetIdx) generator.nodeIdx = oldGenerator.nodeIdx; const [name, statements, templates] = generator.generate(); if (resetIdx) oldGenerator.nodeIdx = generator.nodeIdx; return [name, statements, templates]; } -export function generateBlock(viewUnits: ViewUnit[], config: ViewGeneratorConfig) { +export function generateBlock(viewUnits: ViewUnit[], config: ViewGeneratorConfig, context: ViewGeneratorContext) { const t = config.babelApi.types; const names: string[] = []; const statements = viewUnits.flatMap(viewUnit => { - const generator = new viewGeneratorMap[viewUnit.type](viewUnit, config); + const generator = new viewGeneratorMap[viewUnit.type](viewUnit, config, context); const [name, statements] = generator.generate(); names.push(name); return statements; }); const returnStatement = t.returnStatement(t.arrayExpression(names.map(name => t.identifier(name)))); - return ( - t.blockStatement(statements.concat(returnStatement)) - ); + return t.blockStatement(statements.concat(returnStatement)); } -export function generateView(viewUnits: ViewUnit[], config: ViewGeneratorConfig, templateIdx=-1): [t.Statement[], t.ExpressionStatement] { +function apisCollect() { + const usedApis = new Set(); + return { + collectApis: (name: string) => usedApis.add(name), + getUsedApis: () => [...usedApis], + }; +} + +export function generateView( + viewUnits: ViewUnit[], + config: ViewGeneratorConfig, + templateIdx = -1 +): [t.Statement[], t.ExpressionStatement, string[]] { const t = config.babelApi.types; const names: string[] = []; const allTemplates: t.Statement[] = []; let nodeIdx = -1; + const { collectApis, getUsedApis } = apisCollect(); const statements = viewUnits.flatMap(viewUnit => { - const generator = new viewGeneratorMap[viewUnit.type](viewUnit, config); + const generator = new viewGeneratorMap[viewUnit.type](viewUnit, config, { + collectApis + }); generator.templateIdx = templateIdx; generator.nodeIdx = nodeIdx; const [name, statements, templates] = generator.generate(); @@ -56,17 +72,17 @@ export function generateView(viewUnits: ViewUnit[], config: ViewGeneratorConfig, nodeIdx = generator.nodeIdx; names.push(name); allTemplates.push(...templates); + // merge all imports return statements; }); const returnStatement = t.returnStatement(t.arrayExpression(names.map(name => t.identifier(name)))); - return [allTemplates, ( + return [ + allTemplates, t.expressionStatement( - t.callExpression( - t.arrowFunctionExpression([], t.blockStatement(statements.concat(returnStatement))), - [] - ) - ) - )]; -} \ No newline at end of file + t.callExpression(t.arrowFunctionExpression([], t.blockStatement(statements.concat(returnStatement))), []) + ), + [...getUsedApis()], + ]; +} diff --git a/packages/transpiler/jsx-view-generator/src/test/comp.test.ts b/packages/transpiler/jsx-view-generator/src/test/comp.test.ts index 450cc17a..554820b9 100644 --- a/packages/transpiler/jsx-view-generator/src/test/comp.test.ts +++ b/packages/transpiler/jsx-view-generator/src/test/comp.test.ts @@ -1,49 +1,61 @@ import { describe, it } from 'vitest'; import { expectView } from './mock'; - describe('Comp', () => { it('should generate a Component', () => { - expectView(/*jsx*/` + expectView( + /*jsx*/ ` - `, /*js*/ ` - const $node0 = createComponent(Comp) - `); + `, + /*js*/ ` + const $node0 = runComponent(Comp) + ` + ); }); it('should generate a Component with props', () => { - expectView(/*jsx*/` + expectView( + /*jsx*/ ` - `, /*js*/` - const $node0 = createComponent(Comp, { + `, + /*js*/ ` + const $node0 = runComponent(Comp, { prop1: "value1", prop2: value2 }) - `); + `, + [], + /*apis*/ ['runComponent'] + ); }); it('should generate a Component with children', () => { - expectView(/*jsx*/` + expectView( + /*jsx*/ `
- `, /*js*/` - const $node0 = createComponent(Comp, { + `, + /*js*/ ` + const $node0 = runComponent(Comp, { get children() { const $node0 = createElement("div") return [$node0] } }) - `); + ` + ); }); it('should generate a Component with props and children', () => { - expectView(/*jsx*/` + expectView( + /*jsx*/ `
- `, /*js*/` - const $node0 = createComponent(Comp, { + `, + /*js*/ ` + const $node0 = runComponent(Comp, { prop1: "value1", prop2: value2, get children() { @@ -51,32 +63,41 @@ describe('Comp', () => { return [$node0] } }) - `); + ` + ); }); it('should generate a Component with reactive props', () => { - expectView(/*jsx*/` + expectView( + /*jsx*/ ` - `, /*js*/` - const $node0 = createComponent(Comp, { + `, + /*js*/ ` + const $node0 = runComponent(Comp, { get prop1() { return value.get() } }) - `); + ` + ); }); it('should generate a Component with render/view props', () => { - expectView(/*jsx*/` - ok

}/> - `, /*js*/` - const $node0 = createComponent(Comp, { - render: (() => { - const $node0 = createElement("div") - $node0.textContent = "ok" - return [$node0] - })() - }) - `); + expectView( + /*jsx*/ ` + foo; + `, + /*js*/ ` + const $node0 = runComponent(Dynamic, { + component: "h1", + get children() { + const $node0 = createText("foo"); + return [$node0]; + } + }); + `, + [], + ['createText', 'runComponent'] + ); }); }); diff --git a/packages/transpiler/jsx-view-generator/src/test/const.ts b/packages/transpiler/jsx-view-generator/src/test/const.ts index 9e24bde4..34744cc8 100644 --- a/packages/transpiler/jsx-view-generator/src/test/const.ts +++ b/packages/transpiler/jsx-view-generator/src/test/const.ts @@ -10,7 +10,7 @@ export const importMap = [ 'addEventListener', 'watch', 'insert', - 'createComponent', + 'runComponent', 'createText' ].reduce>((acc, cur) => { acc[cur] = cur; 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 f424392c..bfa10974 100644 --- a/packages/transpiler/jsx-view-generator/src/test/exp.test.ts +++ b/packages/transpiler/jsx-view-generator/src/test/exp.test.ts @@ -26,7 +26,11 @@ describe('Expression', () => { insert($node0, $node2); const $node3 = createText("222"); insert($node0, $node3); - ` + `, [], [ + "createElement", + "createText", + "insert", + ] ); }); @@ -43,7 +47,7 @@ describe('Expression', () => { insert($node1, $node3, $node2); ` , [ - `const $template0 = () => { + `const $template0 = (() => { const $node0 = createElement("div"); const $node1 = createElement("div"); const $node2 = createText("111"); @@ -52,7 +56,7 @@ describe('Expression', () => { insert($node1, $node3); insert($node0, $node1); return $node0; - };` + })();` ] ); }); diff --git a/packages/transpiler/jsx-view-generator/src/test/html.test.ts b/packages/transpiler/jsx-view-generator/src/test/html.test.ts index c47145bd..153c286f 100644 --- a/packages/transpiler/jsx-view-generator/src/test/html.test.ts +++ b/packages/transpiler/jsx-view-generator/src/test/html.test.ts @@ -58,10 +58,11 @@ describe('HTML', () => { it('should generate a div element with a static event', () => { expectView(/*jsx*/` -
+
`, /*js*/` const $node0 = createElement("div") delegateEvent($node0, "click", myFunction) + delegateEvent($node0, "mousedown", otherFn) `); }); @@ -108,7 +109,7 @@ describe('HTML', () => { `, /*js*/` const $node0 = createElement("div") delegateEvent( - $node0, "click", + $node0, "click", () => { console.log(count.get()) } ) `); @@ -121,7 +122,7 @@ describe('HTML', () => {

`, /*js*/` const $node0 = createElement("div") - const $node1 = createComponent(Comp) + const $node1 = runComponent(Comp) insert($node0, $node1) `); }); diff --git a/packages/transpiler/jsx-view-generator/src/test/mock.ts b/packages/transpiler/jsx-view-generator/src/test/mock.ts index ff8629e2..7e6c9f42 100644 --- a/packages/transpiler/jsx-view-generator/src/test/mock.ts +++ b/packages/transpiler/jsx-view-generator/src/test/mock.ts @@ -36,8 +36,8 @@ function formatCode(code: string) { )!.code; } -export function expectView(code: string, expected: string, expectedTemplates?: string[]) { - const [templates, viewAst] = generateView(code); +export function expectView(code: string, expected: string, expectedTemplates?: string[], expectApis?: string[]) { + const [templates, viewAst, apis] = generateView(code); const statements = (((viewAst.expression as t.CallExpression) .callee as t.ArrowFunctionExpression) .body as t.BlockStatement) @@ -45,7 +45,7 @@ export function expectView(code: string, expected: string, expectedTemplates?: s const viewCode = generate( t.file(t.program(statements)), )!.code!; - + const expectedCode = formatCode(expected); expect(viewCode).toBe(expectedCode); @@ -53,7 +53,9 @@ export function expectView(code: string, expected: string, expectedTemplates?: s const templateCode = templates.map(template => generate(template).code); expectedTemplates = expectedTemplates.map(formatCode); expect(templateCode).toEqual(expectedTemplates.map(formatCode)); - + } + if (expectApis) { + expect(expectApis).toEqual(apis); } } @@ -61,4 +63,4 @@ export function js(strings: TemplateStringsArray, ...values: any[]) { return strings.reduce((acc, cur, i) => { return acc + cur + (values[i] || ''); }, ''); -} \ No newline at end of file +} diff --git a/packages/transpiler/jsx-view-generator/src/test/template.test.ts b/packages/transpiler/jsx-view-generator/src/test/template.test.ts index 9378fea8..32ebd206 100644 --- a/packages/transpiler/jsx-view-generator/src/test/template.test.ts +++ b/packages/transpiler/jsx-view-generator/src/test/template.test.ts @@ -158,7 +158,7 @@ describe('Template', () => { `, /*js*/` const $node0 = $template0.cloneNode(true) - const $node1 = createComponent(Comp) + const $node1 = runComponent(Comp) insert($node0, $node1) `, [ /*js*/` @@ -182,7 +182,7 @@ describe('Template', () => { `, /*js*/` const $node0 = $template0.cloneNode(true) const $node1 = $node0.firstChild.nextSibling; - const $node2 = createComponent(Comp); + const $node2 = runComponent(Comp); insert($node0, $node2, $node1); `, [ /*js*/` @@ -238,7 +238,7 @@ describe('Template', () => { }); $node3.className = cls; watch(() => setProperty($node4, "id", id.get())); - const $node5 = createComponent(Comp, { + const $node5 = runComponent(Comp, { myProp: prop, get reactiveProp() { return prop.get(); @@ -249,7 +249,7 @@ describe('Template', () => { const $node7 = $node6.firstChild.nextSibling; const $node8 = $node7.firstChild.nextSibling; watch(() => setProperty($node8, "id", id.get())); - const $node9 = createComponent(Comp); + const $node9 = runComponent(Comp); insert($node6, $node9, $node7); `, [ /*js*/` diff --git a/packages/transpiler/jsx-view-generator/src/types.ts b/packages/transpiler/jsx-view-generator/src/types.ts index 85cf6964..24f952b8 100644 --- a/packages/transpiler/jsx-view-generator/src/types.ts +++ b/packages/transpiler/jsx-view-generator/src/types.ts @@ -11,4 +11,9 @@ export interface ViewGeneratorConfig { * @example { href: ["a", "area", "base", "link"], id: ["*"] } */ attributeMap?: Record -} \ No newline at end of file +} + +type CollectApis = (name: string) => void; +export interface ViewGeneratorContext { + collectApis: CollectApis; +} diff --git a/packages/transpiler/jsx-view-parser/src/parser.ts b/packages/transpiler/jsx-view-parser/src/parser.ts index c27eb92f..23d912e3 100644 --- a/packages/transpiler/jsx-view-parser/src/parser.ts +++ b/packages/transpiler/jsx-view-parser/src/parser.ts @@ -56,7 +56,7 @@ export class ViewParser { this.parse(child); }); } - + return this.viewUnits; } @@ -87,7 +87,7 @@ export class ViewParser { type: 'text', content: node }); - return; + return; } this.viewUnits.push({ type: 'exp', @@ -97,7 +97,7 @@ export class ViewParser { /** * @brief Parse JSXElement - * @param node + * @param node */ private parseElement(node: t.JSXElement): void { let type: 'html' | 'comp'; @@ -109,7 +109,7 @@ export class ViewParser { // ---- Opening name is a JSXIdentifier, e.g.,
const name = openingName.name; // ---- Specially parse if and env - if ([this.ifTagName, this.elseIfTagName, this.elseTagName].includes(name)) + if ([this.ifTagName, this.elseIfTagName, this.elseTagName].includes(name)) return this.parseIf(node); if (name === this.envTagName) return this.parseEnv(node); else if (this.htmlTags.includes(name)) { @@ -184,7 +184,7 @@ export class ViewParser { const childUnits = node.children.map(child => this.parseView(child)).flat(); let unit: ViewUnit = { type, tag, props: propMap, children: childUnits }; - + if (unit.type === 'html' && childUnits.length === 1 && childUnits[0].type === 'text') { // ---- If the html unit only has one text child, merge the text into the html unit const text = childUnits[0] as TextUnit; @@ -236,14 +236,14 @@ export class ViewParser { return; } - const condition = node.openingElement.attributes.filter(attr => + const condition = node.openingElement.attributes.filter(attr => this.t.isJSXAttribute(attr) && attr.name.name === 'cond' )[0]; if (!condition) throw new Error(`Missing condition for ${name}`); if (!this.t.isJSXAttribute(condition)) throw new Error(`JSXSpreadAttribute is not supported for ${name} condition`); if (!this.t.isJSXExpressionContainer(condition.value) || !this.t.isExpression(condition.value.expression)) throw new Error(`Invalid condition for ${name}`); - + // ---- if if (name === this.ifTagName) { this.viewUnits.push({ @@ -254,11 +254,11 @@ export class ViewParser { }], }); return; - } + } // ---- else-if const lastUnit = this.viewUnits[this.viewUnits.length - 1]; - if (!lastUnit || lastUnit.type !== 'if') + if (!lastUnit || lastUnit.type !== 'if') throw new Error(`Missing if for ${name}`); lastUnit.branches.push({ @@ -270,7 +270,7 @@ export class ViewParser { /** * @brief Parse JSXAttribute or JSXSpreadAttribute into UnitProp, * considering both namespace and expression - * @param prop + * @param prop * @returns [propName, propValue] */ private parseJSXProp(prop: t.JSXAttribute | t.JSXSpreadAttribute): [string, UnitProp] { @@ -518,7 +518,7 @@ export class ViewParser { private parseView(node: AllowedJSXNode): ViewUnit[] { return new ViewParser({...this.config, parseTemplate:false}).parse(node); } - + /** * @brief Wrap the value in a file