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