!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

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

@ -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,47 +1,51 @@
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
@ -50,7 +54,7 @@ export default class BaseGenerator {
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];
} }
@ -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,12 +55,12 @@ 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

@ -34,11 +34,12 @@ export class HTMLGenerator extends HTMLPropGenerator {
*/ */
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]
) )
) )
@ -53,6 +54,7 @@ export class HTMLGenerator extends HTMLPropGenerator {
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),

View File

@ -115,6 +115,7 @@ export class TemplateGenerator extends HTMLPropGenerator{
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),

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),

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

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

@ -12,3 +12,8 @@ export interface ViewGeneratorConfig {
*/ */
attributeMap?: Record<string, string[]> attributeMap?: Record<string, string[]>
} }
type CollectApis = (name: string) => void;
export interface ViewGeneratorContext {
collectApis: CollectApis;
}