!159 fix(transpiler): auto import

* fix(transpiler): auto import
* feat(transpiler): auto import
This commit is contained in:
Hoikan 2024-02-23 02:19:37 +00:00 committed by 陈超涛
parent 2640177de5
commit 2445208856
24 changed files with 314 additions and 247 deletions

View File

@ -1,8 +1,8 @@
{ {
"name": "inula-novdom", "name": "@inula/no-vdom",
"version": "0.0.1", "version": "0.0.1",
"description": "no vdom runtime", "description": "no vdom runtime",
"main": "index.js", "main": "./src/index.ts",
"scripts": { "scripts": {
"test": "vitest --ui", "test": "vitest --ui",
"bench": "vitest bench", "bench": "vitest bench",

View File

@ -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';

View File

@ -61,6 +61,7 @@ function watchRender(fn: (value: any) => any, initial?: any): void {
} }
function insertExpression(parent: Node, value: any, prevValue: any, marker?: Node): any { function insertExpression(parent: Node, value: any, prevValue: any, marker?: Node): any {
Array.isArray(value) && value.length === 1 && (value = value[0]);
let result: any; let result: any;
while (typeof prevValue === 'function') { while (typeof prevValue === 'function') {
prevValue = prevValue(); prevValue = prevValue();

View File

@ -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';

View File

@ -17,30 +17,15 @@
import { describe, expect } from 'vitest'; import { describe, expect } from 'vitest';
import { domTest as it } from './utils'; import { domTest as it } from './utils';
import { template as $$template, insert as $$insert } from '../src/dom'; import { render, Dynamic, reactive } from '@inula/no-vdom';
import { runComponent as $$runComponent, render } from '../src/core';
import { Dynamic } from '../src/components/Dynamic';
import { reactive } from 'inula-reactive';
describe('Dynamic', () => { describe('Dynamic', () => {
it('should work with native elements.', ({ container }) => { it('should work with native elements.', ({ container }) => {
/**
*
* function App() {
* return <Dynamic component="h1">foo</Dynamic>;
* }
* render(() => <App />, container);
*/
// 编译后:
function App() { function App() {
return $$runComponent(Dynamic, { return <Dynamic component="h1">foo</Dynamic>;
component: 'h1',
children: 'foo',
});
} }
render(() => $$runComponent(App, {}), container); render(() => <App />, container);
expect(container).toMatchInlineSnapshot(` expect(container).toMatchInlineSnapshot(`
<div> <div>
<h1> <h1>
@ -51,36 +36,15 @@ describe('Dynamic', () => {
}); });
it('should work with components.', ({ container }) => { it('should work with components.', ({ container }) => {
/**
*
* function App() {
* return <Dynamic component={Title} name="bar"/>;
* }
* function Title(props) {
* return <h1>{props.name}</h1>;
* }
* render(() => <App />, container);
*/
// 编译后:
function App() { function App() {
return $$runComponent(Dynamic, { return <Dynamic component={Title} name="bar" />;
component: Title,
name: 'bar',
});
} }
const _tmpl = /*#__PURE__*/ $$template('<h1>');
function Title(props) { function Title(props) {
return (() => { return <h1>{props.name}</h1>;
const _el$ = _tmpl();
$$insert(_el$, () => props.name, null);
return _el$;
})();
} }
render(() => $$runComponent(App, {}), container); render(() => <App />, container);
expect(container).toMatchInlineSnapshot(` expect(container).toMatchInlineSnapshot(`
<div> <div>
<h1> <h1>
@ -91,62 +55,16 @@ describe('Dynamic', () => {
}); });
it('should throw on invalid component.', ({ container }) => { it('should throw on invalid component.', ({ container }) => {
/**
*
* function App() {
* return <Dynamic component={null} />;
* }
* render(() => <App />, container);
*/
// 编译后:
function App() { function App() {
return $$runComponent(Dynamic, { return <Dynamic component={null} />;
component: null,
});
} }
expect(() => render(() => $$runComponent(App, {}), container)).toThrowError('Invalid component for Dynamic'); expect(() => render(() => <App />, container)).toThrowError('Invalid component for Dynamic');
}); });
it('should change component.', async ({ container }) => { it('should change component.', async ({ container }) => {
/** const H1 = props => <h1>{props.children}</h1>;
* const H3 = props => <h3>{props.children}</h3>;
* const H1 = (props) => <h1>{props.children}</h1>;
* const H3 = (props) => <h3>{props.children}</h3>;
* function App() {
* const comp = reactive('h1');
* const comps = {
* H1,
* H3,
* h1: 'h1',
* h2: 'h2',
* }
* return (
* <div>
* <Dynamic component={comps[comp.get()]}>foo</Dynamic>
* </div>
* );
* }
* render(() => <App />, container);
*
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ $$template('<div></div>');
const _h1 = /*#__PURE__*/ $$template('<h1>'),
_h3 = /*#__PURE__*/ $$template('<h3>');
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 comp = reactive('h1'); const comp = reactive('h1');
function App() { function App() {
@ -156,22 +74,15 @@ describe('Dynamic', () => {
h1: 'h1', h1: 'h1',
h2: 'h2', h2: 'h2',
}; };
return (() => { return (
const _div = _tmpl$(); <div>
$$insert( <Dynamic component={comps[comp.get()]}>foo</Dynamic>
_div, </div>
$$runComponent(Dynamic, { );
get component() {
return comps[comp.get()];
},
children: 'foo',
})
);
return _div;
})();
} }
render(() => $$runComponent(App, {}), container); render(() => <App />, container);
expect(container.innerHTML).toMatchInlineSnapshot('"<div><h1>foo</h1></div>"'); expect(container.innerHTML).toMatchInlineSnapshot('"<div><h1>foo</h1></div>"');
comp.set('h2'); comp.set('h2');
expect(container.innerHTML).toMatchInlineSnapshot('"<div><h2>foo</h2></div>"'); expect(container.innerHTML).toMatchInlineSnapshot('"<div><h2>foo</h2></div>"');

View File

@ -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
}
}

View File

@ -16,17 +16,21 @@
// vitest.config.ts // vitest.config.ts
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
import inula from 'vite-plugin-inula-no-vdom'; import inula from 'vite-plugin-inula-no-vdom';
import * as path from 'node:path';
export default defineConfig({ export default defineConfig({
esbuild: { esbuild: {
jsx: 'preserve', jsx: 'preserve',
}, },
resolve: { resolve: {
alias: {
'@inula/no-vdom': path.resolve(__dirname, 'src'),
},
conditions: ['dev'], conditions: ['dev'],
}, },
plugins: [ plugins: [
// @ts-expect-error TODO: fix vite plugin interface is not compatible // @ts-expect-error TODO: fix vite plugin interface is not compatible
inula({ packageName: 'inula-reactive' }), inula(),
], ],
test: { test: {
environment: 'jsdom', // or 'jsdom', 'node' environment: 'jsdom', // or 'jsdom', 'node'

View File

@ -1,16 +1,16 @@
export const importMap = [ export const importMap = [
'createElement', 'createElement',
'setStyle', 'setStyle',
'setAttribute', 'setAttribute',
'setDataset', 'setDataset',
'setProperty', 'setProperty',
'setEvent', 'setEvent',
'delegateEvent', 'delegateEvent',
'addEventListener', 'addEventListener',
'watch', 'watch',
'insert', 'insert',
'createComponent', 'runComponent',
'createText' 'createText'
].reduce<Record<string, string>>((acc, cur) => { ].reduce<Record<string, string>>((acc, cur) => {
acc[cur] = cur; acc[cur] = cur;
@ -510,4 +510,4 @@ export const attributeMap = {
ariaRowIndex: ['*'], ariaRowIndex: ['*'],
ariaRowSpan: ['*'], ariaRowSpan: ['*'],
ariaSetSize: ['*'], ariaSetSize: ['*'],
}; };

View File

@ -6,9 +6,8 @@ import { generateView } from '@inula/jsx-view-generator';
import { parseView } from '@inula/jsx-view-parser'; import { parseView } from '@inula/jsx-view-parser';
import { attributeMap, htmlTags, importMap } from './const'; import { attributeMap, htmlTags, importMap } from './const';
export class PluginProvider { export class PluginProvider {
private inulaPackageName = 'inula-reactive'; private inulaPackageName = '@inula/no-vdom';
// ---- Plugin Level ---- // ---- Plugin Level ----
private readonly babelApi: typeof babel private readonly babelApi: typeof babel
private readonly t: typeof t private readonly t: typeof t
@ -47,6 +46,9 @@ export class PluginProvider {
private templateIdx = -1 private templateIdx = -1
// ---- record used apis for automatic import
private allUsedApis: Set<string> = new Set<string>();
programEnterVisitor( programEnterVisitor(
path: NodePath<t.Program>, path: NodePath<t.Program>,
filename: string | undefined filename: string | undefined
@ -71,8 +73,19 @@ export class PluginProvider {
programExitVisitor(path: NodePath<t.Program>): void { programExitVisitor(path: NodePath<t.Program>): void {
if (!this.fileEnter) return; if (!this.fileEnter) return;
this.fileEnter = false; 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<t.JSXElement | t.JSXFragment>): void { jsxElementVisitor(path: NodePath<t.JSXElement | t.JSXFragment>): void {
if (!this.fileEnter) return; if (!this.fileEnter) return;
@ -81,7 +94,7 @@ export class PluginProvider {
htmlTags: this.htmlTags, htmlTags: this.htmlTags,
parseTemplate: this.parseTemplate, parseTemplate: this.parseTemplate,
}); });
const [templates, viewAst] = generateView(viewUnits, { const [templates, viewAst, usedApis] = generateView(viewUnits, {
babelApi: this.babelApi, babelApi: this.babelApi,
importMap, importMap,
attributeMap: this.attributeMap, attributeMap: this.attributeMap,
@ -89,7 +102,8 @@ export class PluginProvider {
this.templateIdx += templates.length; this.templateIdx += templates.length;
// ---- Add templates to the program // ---- Add templates to the program
this.programNode!.body.unshift(...templates); this.programNode!.body.unshift(...templates);
// ---- collect the used apis
usedApis.forEach(api => this.allUsedApis.add(api))
// ---- Replace the JSXElement with the viewAst // ---- Replace the JSXElement with the viewAst
path.replaceWith(viewAst); path.replaceWith(viewAst);
path.skip(); path.skip();

View File

@ -1,67 +1,71 @@
import { UnitProp, ViewUnit } from '@inula/jsx-view-parser'; 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'; import type { types as t, traverse } from '@babel/core';
export default class BaseGenerator { export default class BaseGenerator {
readonly viewUnit: ViewUnit readonly viewUnit: ViewUnit;
readonly config: ViewGeneratorConfig readonly config: ViewGeneratorConfig;
readonly t readonly t;
readonly traverse: typeof traverse readonly traverse: typeof traverse;
readonly elementAttributeMap readonly elementAttributeMap;
readonly importMap readonly importMap;
readonly context : ViewGeneratorContext;
constructor(viewUnit: ViewUnit, config: ViewGeneratorConfig) { constructor(viewUnit: ViewUnit, config: ViewGeneratorConfig, context: ViewGeneratorContext) {
this.config = config; this.config = config;
this.t = config.babelApi.types; this.t = config.babelApi.types;
this.traverse = config.babelApi.traverse; this.traverse = config.babelApi.traverse;
this.importMap = config.importMap; this.importMap = config.importMap;
this.viewUnit = viewUnit; this.viewUnit = viewUnit;
this.elementAttributeMap = config.attributeMap this.elementAttributeMap = config.attributeMap
? Object.entries(config.attributeMap).reduce<Record<string, string[]>>( ? Object.entries(config.attributeMap).reduce<Record<string, string[]>>((acc, [key, elements]) => {
(acc, [key, elements]) => { elements.forEach(element => {
elements.forEach(element => { if (!acc[element]) acc[element] = [];
if (!acc[element]) acc[element] = []; acc[element].push(key);
acc[element].push(key); });
}); return acc;
return acc; }, {})
},
{}
)
: {}; : {};
this.context = context;
} }
// ---- Init Statements // ---- Init Statements
private readonly initStatements: t.Statement[] = [] private readonly initStatements: t.Statement[] = [];
addStatement(...statements: t.Statement[]) { addStatement(...statements: t.Statement[]) {
this.initStatements.push(...statements); this.initStatements.push(...statements);
} }
private readonly templates: t.Statement[] = [] private readonly templates: t.Statement[] = [];
addTemplate(...template: t.Statement[]) { addTemplate(...template: t.Statement[]) {
this.templates.push(...template); this.templates.push(...template);
} }
addUsedApi(apiName: string) {
this.context.collectApis(apiName);
}
// ---- Generate ---- // ---- Generate ----
/** /**
* @brief To be implemented by the subclass * @brief To be implemented by the subclass
*/ */
run(): string { run(): string {
return ''; return '';
} }
generate(): [string, t.Statement[], t.Statement[]]{ generate(): [string, t.Statement[], t.Statement[]] {
const nodeName = this.run(); const nodeName = this.run();
return [nodeName, this.initStatements, this.templates]; return [nodeName, this.initStatements, this.templates];
} }
// ---- Reactivity ---- // ---- Reactivity ----
/** /**
* @brief Check if the expression is reactive, which satisfies the following conditions: * @brief Check if the expression is reactive, which satisfies the following conditions:
* 1. Contains .get() property * 1. Contains .get() property
* 2. The whole expression is not a function * 2. The whole expression is not a function
* @param expression * @param expression
* @returns * @returns
*/ */
checkReactive(expression: t.Expression) { checkReactive(expression: t.Expression) {
if (this.t.isFunction(expression)) return false; if (this.t.isFunction(expression)) return false;
@ -72,7 +76,7 @@ export default class BaseGenerator {
reactive = true; reactive = true;
path.stop(); path.stop();
} }
} },
}); });
return reactive; return reactive;
} }
@ -81,18 +85,20 @@ export default class BaseGenerator {
private readonly prefixMap = { private readonly prefixMap = {
node: '$node', node: '$node',
template: '$template', template: '$template',
} };
nodeIdx = -1; nodeIdx = -1;
geneNodeName(idx?: number): string { geneNodeName(idx?: number): string {
return `${this.prefixMap.node}${idx ?? ++this.nodeIdx}`; return `${this.prefixMap.node}${idx ?? ++this.nodeIdx}`;
} }
templateIdx = -1; templateIdx = -1;
generateTemplateName() { generateTemplateName() {
return `${this.prefixMap.template}${++this.templateIdx}`; return `${this.prefixMap.template}${++this.templateIdx}`;
} }
// ---- Utils ---- // ---- Utils ----
/** /**
* @brief Wrap the value in a file * @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[] = []; const statements: t.Statement[] = [];
function collect(statement: t.Statement | t.Statement[] | null) { function collect(statement: t.Statement | t.Statement[] | null) {
if (Array.isArray(statement)) { if (Array.isArray(statement)) {
statements.push(...statement); statements.push(...statement);
@ -119,10 +126,18 @@ export default class BaseGenerator {
statements.push(statement); statements.push(statement);
} }
} }
return [statements, collect]; 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; let value = prop.value;
const viewPropMap = prop.viewPropMap; const viewPropMap = prop.viewPropMap;
const propNodeMap = Object.fromEntries( const propNodeMap = Object.fromEntries(
@ -142,13 +157,16 @@ export default class BaseGenerator {
} }
path.replaceWith(propNodeMap[key]); path.replaceWith(propNodeMap[key]);
} }
} },
}); });
return value; return value;
} }
parseProps(props: Record<string, UnitProp>, generateView: (units: ViewUnit[], config: ViewGeneratorConfig) => [t.Statement[], t.ExpressionStatement]) { parseProps(
props: Record<string, UnitProp>,
generateView: (units: ViewUnit[], config: ViewGeneratorConfig) => [t.Statement[], t.ExpressionStatement, string[]]
) {
return Object.fromEntries( return Object.fromEntries(
Object.entries(props).map(([key, prop]) => { Object.entries(props).map(([key, prop]) => {
return [key, this.parseViewProp(prop, generateView)]; return [key, this.parseViewProp(prop, generateView)];

View File

@ -48,6 +48,7 @@ export class HTMLPropGenerator extends BaseGenerator {
nodeName: string, nodeName: string,
value: t.Expression, value: t.Expression,
) { ) {
this.addUsedApi(this.importMap.setStyle);
return this.t.callExpression( return this.t.callExpression(
this.t.identifier(this.importMap.setStyle), this.t.identifier(this.importMap.setStyle),
[this.t.identifier(nodeName), value] [this.t.identifier(nodeName), value]
@ -62,6 +63,7 @@ export class HTMLPropGenerator extends BaseGenerator {
nodeName: string, nodeName: string,
value: t.Expression, value: t.Expression,
) { ) {
this.addUsedApi(this.importMap.setDataset);
return this.t.callExpression( return this.t.callExpression(
this.t.identifier(this.importMap.setDataset), this.t.identifier(this.importMap.setDataset),
[this.t.identifier(nodeName), value] [this.t.identifier(nodeName), value]
@ -96,6 +98,7 @@ export class HTMLPropGenerator extends BaseGenerator {
key: string, key: string,
value: t.Expression, value: t.Expression,
) { ) {
this.addUsedApi(this.importMap.setProperty);
return this.t.callExpression( return this.t.callExpression(
this.t.identifier(this.importMap.setProperty), this.t.identifier(this.importMap.setProperty),
[this.t.identifier(nodeName), this.t.stringLiteral(key), value] [this.t.identifier(nodeName), this.t.stringLiteral(key), value]
@ -130,6 +133,7 @@ export class HTMLPropGenerator extends BaseGenerator {
key: string, key: string,
value: t.Expression, value: t.Expression,
) { ) {
this.addUsedApi(this.importMap.setAttribute);
return this.t.callExpression( return this.t.callExpression(
this.t.identifier(this.importMap.setAttribute), this.t.identifier(this.importMap.setAttribute),
[this.t.identifier(nodeName), this.t.stringLiteral(key), value] [this.t.identifier(nodeName), this.t.stringLiteral(key), value]
@ -145,6 +149,7 @@ export class HTMLPropGenerator extends BaseGenerator {
eventName: string, eventName: string,
value: t.Expression, value: t.Expression,
) { ) {
this.addUsedApi(this.importMap.delegateEvent);
return this.t.callExpression( return this.t.callExpression(
this.t.identifier(this.importMap.delegateEvent), this.t.identifier(this.importMap.delegateEvent),
[this.t.identifier(nodeName), this.t.stringLiteral(eventName), value] [this.t.identifier(nodeName), this.t.stringLiteral(eventName), value]
@ -178,6 +183,7 @@ export class HTMLPropGenerator extends BaseGenerator {
eventName: string, eventName: string,
value: t.Expression, value: t.Expression,
) { ) {
this.addUsedApi(this.importMap.addEventListener);
return this.t.callExpression( return this.t.callExpression(
this.t.identifier(this.importMap.addEventListener), this.t.identifier(this.importMap.addEventListener),
[this.t.identifier(nodeName), this.t.stringLiteral(eventName), value] [this.t.identifier(nodeName), this.t.stringLiteral(eventName), value]

View File

@ -16,7 +16,7 @@ export class CompGenerator extends BaseGenerator {
/** /**
* @View * @View
* const $el = createComponent(tag, { * const $el = runComponent(tag, {
* ...props * ...props
* }, spreadProps) * }, spreadProps)
*/ */
@ -47,7 +47,7 @@ export class CompGenerator extends BaseGenerator {
); );
})); }));
if (children.length > 0) { if (children.length > 0) {
const statement = generateBlock(children, this.config); const statement = generateBlock(children, this.config, this.context);
propNode.properties.push( propNode.properties.push(
this.t.objectMethod('get', this.t.identifier('children'), [], statement) this.t.objectMethod('get', this.t.identifier('children'), [], statement)
); );
@ -55,15 +55,15 @@ export class CompGenerator extends BaseGenerator {
nodes.push(propNode); nodes.push(propNode);
} }
this.addUsedApi(this.importMap.runComponent);
return [name, this.t.variableDeclaration('const', [ return [name, this.t.variableDeclaration('const', [
this.t.variableDeclarator( this.t.variableDeclarator(
this.t.identifier(name), this.t.identifier(name),
this.t.callExpression( this.t.callExpression(
this.t.identifier(this.importMap.createComponent), this.t.identifier(this.importMap.runComponent),
[tag, ...nodes] [tag, ...nodes]
) )
) )
])]; ])];
} }
} }

View File

@ -29,16 +29,17 @@ export class HTMLGenerator extends HTMLPropGenerator {
} }
/** /**
* @View * @View
* const $el = createElement(tag) * const $el = createElement(tag)
*/ */
declareHTMLNode(tag: t.Expression): [string, t.Statement] { declareHTMLNode(tag: t.Expression): [string, t.Statement] {
const name = this.geneNodeName(); const name = this.geneNodeName();
this.addUsedApi(this.importMap.createElement);
return [name, this.t.variableDeclaration('const', [ return [name, this.t.variableDeclaration('const', [
this.t.variableDeclarator( this.t.variableDeclarator(
this.t.identifier(name), this.t.identifier(name),
this.t.callExpression( this.t.callExpression(
this.t.identifier('createElement'), this.t.identifier(this.importMap.createElement),
[tag] [tag]
) )
) )
@ -50,9 +51,10 @@ export class HTMLGenerator extends HTMLPropGenerator {
* $insert($el, childNode) * $insert($el, childNode)
*/ */
private insertChildNode( private insertChildNode(
parent: string, parent: string,
child: string child: string
) { ) {
this.addUsedApi(this.importMap.insert);
return this.t.expressionStatement( return this.t.expressionStatement(
this.t.callExpression( this.t.callExpression(
this.t.identifier(this.importMap.insert), this.t.identifier(this.importMap.insert),
@ -60,4 +62,4 @@ export class HTMLGenerator extends HTMLPropGenerator {
) )
); );
} }
} }

View File

@ -110,11 +110,12 @@ export class TemplateGenerator extends HTMLPropGenerator{
* $insert($el, childNode) * $insert($el, childNode)
*/ */
private insertChildNode( private insertChildNode(
parent: string, parent: string,
child: string, child: string,
nextName: string nextName: string
) { ) {
const nextNode = nextName ? [this.t.identifier(nextName)] : []; const nextNode = nextName ? [this.t.identifier(nextName)] : [];
this.addUsedApi(this.importMap.insert);
return this.t.expressionStatement( return this.t.expressionStatement(
this.t.callExpression( this.t.callExpression(
this.t.identifier(this.importMap.insert), this.t.identifier(this.importMap.insert),
@ -319,4 +320,4 @@ export class TemplateGenerator extends HTMLPropGenerator{
} }
return [bestMatchName, path.slice(bestMatchCount), 0]; return [bestMatchName, path.slice(bestMatchCount), 0];
} }
} }

View File

@ -12,6 +12,7 @@ export class TextGenerator extends BaseGenerator {
declareTextNode(content: t.Literal): [string, t.Statement] { declareTextNode(content: t.Literal): [string, t.Statement] {
const name = this.geneNodeName(); const name = this.geneNodeName();
this.addUsedApi(this.importMap.createText);
return [name, this.t.variableDeclaration('const', [ return [name, this.t.variableDeclaration('const', [
this.t.variableDeclarator( this.t.variableDeclarator(
this.t.identifier(name), this.t.identifier(name),
@ -22,4 +23,4 @@ export class TextGenerator extends BaseGenerator {
) )
])]; ])];
} }
} }

View File

@ -1,7 +1,7 @@
import { ViewUnit } from '@inula/jsx-view-parser'; import { ViewUnit } from '@inula/jsx-view-parser';
import { CompGenerator } from './NodeGenerators/CompGenerator'; import { CompGenerator } from './NodeGenerators/CompGenerator';
import { HTMLGenerator } from './NodeGenerators/HTMLGenerator'; import { HTMLGenerator } from './NodeGenerators/HTMLGenerator';
import { ViewGeneratorConfig } from './types'; import { ViewGeneratorConfig, ViewGeneratorContext } from './types';
import type { types as t } from '@babel/core'; import type { types as t } from '@babel/core';
import { TemplateGenerator } from './NodeGenerators/TemplateGenerator'; import { TemplateGenerator } from './NodeGenerators/TemplateGenerator';
import { TextGenerator } from './NodeGenerators/TextGenerator'; import { TextGenerator } from './NodeGenerators/TextGenerator';
@ -17,38 +17,54 @@ export const viewGeneratorMap = {
env: CompGenerator, env: CompGenerator,
} as const; } as const;
export function generateNew(
export function generateNew(oldGenerator: any, viewUnit: ViewUnit, resetIdx = true): [string, t.Statement[], t.Statement[]]{ oldGenerator: any,
const generator = new viewGeneratorMap[viewUnit.type](viewUnit, oldGenerator.config); 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; if (resetIdx) generator.nodeIdx = oldGenerator.nodeIdx;
const [name, statements, templates] = generator.generate(); const [name, statements, templates] = generator.generate();
if (resetIdx) oldGenerator.nodeIdx = generator.nodeIdx; if (resetIdx) oldGenerator.nodeIdx = generator.nodeIdx;
return [name, statements, templates]; 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 t = config.babelApi.types;
const names: string[] = []; const names: string[] = [];
const statements = viewUnits.flatMap(viewUnit => { 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(); const [name, statements] = generator.generate();
names.push(name); names.push(name);
return statements; return statements;
}); });
const returnStatement = t.returnStatement(t.arrayExpression(names.map(name => t.identifier(name)))); const returnStatement = t.returnStatement(t.arrayExpression(names.map(name => t.identifier(name))));
return ( return t.blockStatement(statements.concat(returnStatement));
t.blockStatement(statements.concat(returnStatement))
);
} }
export function generateView(viewUnits: ViewUnit[], config: ViewGeneratorConfig, templateIdx=-1): [t.Statement[], t.ExpressionStatement] { function apisCollect() {
const usedApis = new Set<string>();
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 t = config.babelApi.types;
const names: string[] = []; const names: string[] = [];
const allTemplates: t.Statement[] = []; const allTemplates: t.Statement[] = [];
let nodeIdx = -1; let nodeIdx = -1;
const { collectApis, getUsedApis } = apisCollect();
const statements = viewUnits.flatMap(viewUnit => { 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.templateIdx = templateIdx;
generator.nodeIdx = nodeIdx; generator.nodeIdx = nodeIdx;
const [name, statements, templates] = generator.generate(); const [name, statements, templates] = generator.generate();
@ -56,17 +72,17 @@ export function generateView(viewUnits: ViewUnit[], config: ViewGeneratorConfig,
nodeIdx = generator.nodeIdx; nodeIdx = generator.nodeIdx;
names.push(name); names.push(name);
allTemplates.push(...templates); allTemplates.push(...templates);
// merge all imports
return statements; return statements;
}); });
const returnStatement = t.returnStatement(t.arrayExpression(names.map(name => t.identifier(name)))); const returnStatement = t.returnStatement(t.arrayExpression(names.map(name => t.identifier(name))));
return [allTemplates, ( return [
allTemplates,
t.expressionStatement( t.expressionStatement(
t.callExpression( t.callExpression(t.arrowFunctionExpression([], t.blockStatement(statements.concat(returnStatement))), [])
t.arrowFunctionExpression([], t.blockStatement(statements.concat(returnStatement))), ),
[] [...getUsedApis()],
) ];
) }
)];
}

View File

@ -1,49 +1,61 @@
import { describe, it } from 'vitest'; import { describe, it } from 'vitest';
import { expectView } from './mock'; import { expectView } from './mock';
describe('Comp', () => { describe('Comp', () => {
it('should generate a Component', () => { it('should generate a Component', () => {
expectView(/*jsx*/` expectView(
/*jsx*/ `
<Comp/> <Comp/>
`, /*js*/ ` `,
const $node0 = createComponent(Comp) /*js*/ `
`); const $node0 = runComponent(Comp)
`
);
}); });
it('should generate a Component with props', () => { it('should generate a Component with props', () => {
expectView(/*jsx*/` expectView(
/*jsx*/ `
<Comp prop1="value1" prop2={value2}/> <Comp prop1="value1" prop2={value2}/>
`, /*js*/` `,
const $node0 = createComponent(Comp, { /*js*/ `
const $node0 = runComponent(Comp, {
prop1: "value1", prop1: "value1",
prop2: value2 prop2: value2
}) })
`); `,
[],
/*apis*/ ['runComponent']
);
}); });
it('should generate a Component with children', () => { it('should generate a Component with children', () => {
expectView(/*jsx*/` expectView(
/*jsx*/ `
<Comp> <Comp>
<div></div> <div></div>
</Comp> </Comp>
`, /*js*/` `,
const $node0 = createComponent(Comp, { /*js*/ `
const $node0 = runComponent(Comp, {
get children() { get children() {
const $node0 = createElement("div") const $node0 = createElement("div")
return [$node0] return [$node0]
} }
}) })
`); `
);
}); });
it('should generate a Component with props and children', () => { it('should generate a Component with props and children', () => {
expectView(/*jsx*/` expectView(
/*jsx*/ `
<Comp prop1="value1" prop2={value2}> <Comp prop1="value1" prop2={value2}>
<div></div> <div></div>
</Comp> </Comp>
`, /*js*/` `,
const $node0 = createComponent(Comp, { /*js*/ `
const $node0 = runComponent(Comp, {
prop1: "value1", prop1: "value1",
prop2: value2, prop2: value2,
get children() { get children() {
@ -51,32 +63,41 @@ describe('Comp', () => {
return [$node0] return [$node0]
} }
}) })
`); `
);
}); });
it('should generate a Component with reactive props', () => { it('should generate a Component with reactive props', () => {
expectView(/*jsx*/` expectView(
/*jsx*/ `
<Comp prop1={value.get()}/> <Comp prop1={value.get()}/>
`, /*js*/` `,
const $node0 = createComponent(Comp, { /*js*/ `
const $node0 = runComponent(Comp, {
get prop1() { get prop1() {
return value.get() return value.get()
} }
}) })
`); `
);
}); });
it('should generate a Component with render/view props', () => { it('should generate a Component with render/view props', () => {
expectView(/*jsx*/` expectView(
<Comp render={<div>ok</div>}/> /*jsx*/ `
`, /*js*/` <Dynamic component="h1">foo</Dynamic>;
const $node0 = createComponent(Comp, { `,
render: (() => { /*js*/ `
const $node0 = createElement("div") const $node0 = runComponent(Dynamic, {
$node0.textContent = "ok" component: "h1",
return [$node0] get children() {
})() const $node0 = createText("foo");
}) return [$node0];
`); }
});
`,
[],
['createText', 'runComponent']
);
}); });
}); });

View File

@ -10,7 +10,7 @@ export const importMap = [
'addEventListener', 'addEventListener',
'watch', 'watch',
'insert', 'insert',
'createComponent', 'runComponent',
'createText' 'createText'
].reduce<Record<string, string>>((acc, cur) => { ].reduce<Record<string, string>>((acc, cur) => {
acc[cur] = cur; acc[cur] = cur;

View File

@ -26,7 +26,11 @@ describe('Expression', () => {
insert($node0, $node2); insert($node0, $node2);
const $node3 = createText("222"); const $node3 = createText("222");
insert($node0, $node3); insert($node0, $node3);
` `, [], [
"createElement",
"createText",
"insert",
]
); );
}); });
@ -43,7 +47,7 @@ describe('Expression', () => {
insert($node1, $node3, $node2); insert($node1, $node3, $node2);
` `
, [ , [
`const $template0 = () => { `const $template0 = (() => {
const $node0 = createElement("div"); const $node0 = createElement("div");
const $node1 = createElement("div"); const $node1 = createElement("div");
const $node2 = createText("111"); const $node2 = createText("111");
@ -52,7 +56,7 @@ describe('Expression', () => {
insert($node1, $node3); insert($node1, $node3);
insert($node0, $node1); insert($node0, $node1);
return $node0; return $node0;
};` })();`
] ]
); );
}); });

View File

@ -58,10 +58,11 @@ describe('HTML', () => {
it('should generate a div element with a static event', () => { it('should generate a div element with a static event', () => {
expectView(/*jsx*/` expectView(/*jsx*/`
<div onClick={myFunction}></div> <div onClick={myFunction} onMouseDown={otherFn}></div>
`, /*js*/` `, /*js*/`
const $node0 = createElement("div") const $node0 = createElement("div")
delegateEvent($node0, "click", myFunction) delegateEvent($node0, "click", myFunction)
delegateEvent($node0, "mousedown", otherFn)
`); `);
}); });
@ -108,7 +109,7 @@ describe('HTML', () => {
`, /*js*/` `, /*js*/`
const $node0 = createElement("div") const $node0 = createElement("div")
delegateEvent( delegateEvent(
$node0, "click", $node0, "click",
() => { console.log(count.get()) } () => { console.log(count.get()) }
) )
`); `);
@ -121,7 +122,7 @@ describe('HTML', () => {
</div> </div>
`, /*js*/` `, /*js*/`
const $node0 = createElement("div") const $node0 = createElement("div")
const $node1 = createComponent(Comp) const $node1 = runComponent(Comp)
insert($node0, $node1) insert($node0, $node1)
`); `);
}); });

View File

@ -36,8 +36,8 @@ function formatCode(code: string) {
)!.code; )!.code;
} }
export function expectView(code: string, expected: string, expectedTemplates?: string[]) { export function expectView(code: string, expected: string, expectedTemplates?: string[], expectApis?: string[]) {
const [templates, viewAst] = generateView(code); const [templates, viewAst, apis] = generateView(code);
const statements = (((viewAst.expression as t.CallExpression) const statements = (((viewAst.expression as t.CallExpression)
.callee as t.ArrowFunctionExpression) .callee as t.ArrowFunctionExpression)
.body as t.BlockStatement) .body as t.BlockStatement)
@ -45,7 +45,7 @@ export function expectView(code: string, expected: string, expectedTemplates?: s
const viewCode = generate( const viewCode = generate(
t.file(t.program(statements)), t.file(t.program(statements)),
)!.code!; )!.code!;
const expectedCode = formatCode(expected); const expectedCode = formatCode(expected);
expect(viewCode).toBe(expectedCode); 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); const templateCode = templates.map(template => generate(template).code);
expectedTemplates = expectedTemplates.map(formatCode); expectedTemplates = expectedTemplates.map(formatCode);
expect(templateCode).toEqual(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 strings.reduce((acc, cur, i) => {
return acc + cur + (values[i] || ''); return acc + cur + (values[i] || '');
}, ''); }, '');
} }

View File

@ -158,7 +158,7 @@ describe('Template', () => {
</div> </div>
`, /*js*/` `, /*js*/`
const $node0 = $template0.cloneNode(true) const $node0 = $template0.cloneNode(true)
const $node1 = createComponent(Comp) const $node1 = runComponent(Comp)
insert($node0, $node1) insert($node0, $node1)
`, [ `, [
/*js*/` /*js*/`
@ -182,7 +182,7 @@ describe('Template', () => {
`, /*js*/` `, /*js*/`
const $node0 = $template0.cloneNode(true) const $node0 = $template0.cloneNode(true)
const $node1 = $node0.firstChild.nextSibling; const $node1 = $node0.firstChild.nextSibling;
const $node2 = createComponent(Comp); const $node2 = runComponent(Comp);
insert($node0, $node2, $node1); insert($node0, $node2, $node1);
`, [ `, [
/*js*/` /*js*/`
@ -238,7 +238,7 @@ describe('Template', () => {
}); });
$node3.className = cls; $node3.className = cls;
watch(() => setProperty($node4, "id", id.get())); watch(() => setProperty($node4, "id", id.get()));
const $node5 = createComponent(Comp, { const $node5 = runComponent(Comp, {
myProp: prop, myProp: prop,
get reactiveProp() { get reactiveProp() {
return prop.get(); return prop.get();
@ -249,7 +249,7 @@ describe('Template', () => {
const $node7 = $node6.firstChild.nextSibling; const $node7 = $node6.firstChild.nextSibling;
const $node8 = $node7.firstChild.nextSibling; const $node8 = $node7.firstChild.nextSibling;
watch(() => setProperty($node8, "id", id.get())); watch(() => setProperty($node8, "id", id.get()));
const $node9 = createComponent(Comp); const $node9 = runComponent(Comp);
insert($node6, $node9, $node7); insert($node6, $node9, $node7);
`, [ `, [
/*js*/` /*js*/`

View File

@ -11,4 +11,9 @@ export interface ViewGeneratorConfig {
* @example { href: ["a", "area", "base", "link"], id: ["*"] } * @example { href: ["a", "area", "base", "link"], id: ["*"] }
*/ */
attributeMap?: Record<string, string[]> attributeMap?: Record<string, string[]>
} }
type CollectApis = (name: string) => void;
export interface ViewGeneratorContext {
collectApis: CollectApis;
}

View File

@ -56,7 +56,7 @@ export class ViewParser {
this.parse(child); this.parse(child);
}); });
} }
return this.viewUnits; return this.viewUnits;
} }
@ -87,7 +87,7 @@ export class ViewParser {
type: 'text', type: 'text',
content: node content: node
}); });
return; return;
} }
this.viewUnits.push({ this.viewUnits.push({
type: 'exp', type: 'exp',
@ -97,7 +97,7 @@ export class ViewParser {
/** /**
* @brief Parse JSXElement * @brief Parse JSXElement
* @param node * @param node
*/ */
private parseElement(node: t.JSXElement): void { private parseElement(node: t.JSXElement): void {
let type: 'html' | 'comp'; let type: 'html' | 'comp';
@ -109,7 +109,7 @@ export class ViewParser {
// ---- Opening name is a JSXIdentifier, e.g., <div> // ---- Opening name is a JSXIdentifier, e.g., <div>
const name = openingName.name; const name = openingName.name;
// ---- Specially parse if and env // ---- 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); return this.parseIf(node);
if (name === this.envTagName) return this.parseEnv(node); if (name === this.envTagName) return this.parseEnv(node);
else if (this.htmlTags.includes(name)) { else if (this.htmlTags.includes(name)) {
@ -184,7 +184,7 @@ export class ViewParser {
const childUnits = node.children.map(child => this.parseView(child)).flat(); const childUnits = node.children.map(child => this.parseView(child)).flat();
let unit: ViewUnit = { type, tag, props: propMap, children: childUnits }; let unit: ViewUnit = { type, tag, props: propMap, children: childUnits };
if (unit.type === 'html' && childUnits.length === 1 && childUnits[0].type === 'text') { 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 // ---- If the html unit only has one text child, merge the text into the html unit
const text = childUnits[0] as TextUnit; const text = childUnits[0] as TextUnit;
@ -236,14 +236,14 @@ export class ViewParser {
return; return;
} }
const condition = node.openingElement.attributes.filter(attr => const condition = node.openingElement.attributes.filter(attr =>
this.t.isJSXAttribute(attr) && attr.name.name === 'cond' this.t.isJSXAttribute(attr) && attr.name.name === 'cond'
)[0]; )[0];
if (!condition) throw new Error(`Missing condition for ${name}`); 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.isJSXAttribute(condition)) throw new Error(`JSXSpreadAttribute is not supported for ${name} condition`);
if (!this.t.isJSXExpressionContainer(condition.value) || !this.t.isExpression(condition.value.expression)) if (!this.t.isJSXExpressionContainer(condition.value) || !this.t.isExpression(condition.value.expression))
throw new Error(`Invalid condition for ${name}`); throw new Error(`Invalid condition for ${name}`);
// ---- if // ---- if
if (name === this.ifTagName) { if (name === this.ifTagName) {
this.viewUnits.push({ this.viewUnits.push({
@ -254,11 +254,11 @@ export class ViewParser {
}], }],
}); });
return; return;
} }
// ---- else-if // ---- else-if
const lastUnit = this.viewUnits[this.viewUnits.length - 1]; 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}`); throw new Error(`Missing if for ${name}`);
lastUnit.branches.push({ lastUnit.branches.push({
@ -270,7 +270,7 @@ export class ViewParser {
/** /**
* @brief Parse JSXAttribute or JSXSpreadAttribute into UnitProp, * @brief Parse JSXAttribute or JSXSpreadAttribute into UnitProp,
* considering both namespace and expression * considering both namespace and expression
* @param prop * @param prop
* @returns [propName, propValue] * @returns [propName, propValue]
*/ */
private parseJSXProp(prop: t.JSXAttribute | t.JSXSpreadAttribute): [string, UnitProp] { private parseJSXProp(prop: t.JSXAttribute | t.JSXSpreadAttribute): [string, UnitProp] {
@ -518,7 +518,7 @@ export class ViewParser {
private parseView(node: AllowedJSXNode): ViewUnit[] { private parseView(node: AllowedJSXNode): ViewUnit[] {
return new ViewParser({...this.config, parseTemplate:false}).parse(node); return new ViewParser({...this.config, parseTemplate:false}).parse(node);
} }
/** /**
* @brief Wrap the value in a file * @brief Wrap the value in a file