!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",
"description": "no vdom runtime",
"main": "index.js",
"main": "./src/index.ts",
"scripts": {
"test": "vitest --ui",
"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 {
Array.isArray(value) && value.length === 1 && (value = value[0]);
let result: any;
while (typeof prevValue === 'function') {
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 { 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 <Dynamic component="h1">foo</Dynamic>;
* }
* render(() => <App />, container);
*/
// 编译后:
function App() {
return $$runComponent(Dynamic, {
component: 'h1',
children: 'foo',
});
return <Dynamic component="h1">foo</Dynamic>;
}
render(() => $$runComponent(App, {}), container);
render(() => <App />, container);
expect(container).toMatchInlineSnapshot(`
<div>
<h1>
@ -51,36 +36,15 @@ describe('Dynamic', () => {
});
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() {
return $$runComponent(Dynamic, {
component: Title,
name: 'bar',
});
return <Dynamic component={Title} name="bar" />;
}
const _tmpl = /*#__PURE__*/ $$template('<h1>');
function Title(props) {
return (() => {
const _el$ = _tmpl();
$$insert(_el$, () => props.name, null);
return _el$;
})();
return <h1>{props.name}</h1>;
}
render(() => $$runComponent(App, {}), container);
render(() => <App />, container);
expect(container).toMatchInlineSnapshot(`
<div>
<h1>
@ -91,62 +55,16 @@ describe('Dynamic', () => {
});
it('should throw on invalid component.', ({ container }) => {
/**
*
* function App() {
* return <Dynamic component={null} />;
* }
* render(() => <App />, container);
*/
// 编译后:
function App() {
return $$runComponent(Dynamic, {
component: null,
});
return <Dynamic 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 }) => {
/**
*
* 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 H1 = props => <h1>{props.children}</h1>;
const H3 = props => <h3>{props.children}</h3>;
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 (
<div>
<Dynamic component={comps[comp.get()]}>foo</Dynamic>
</div>
);
}
render(() => $$runComponent(App, {}), container);
render(() => <App />, container);
expect(container.innerHTML).toMatchInlineSnapshot('"<div><h1>foo</h1></div>"');
comp.set('h2');
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
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'

View File

@ -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<Record<string, string>>((acc, cur) => {
acc[cur] = cur;
@ -510,4 +510,4 @@ export const attributeMap = {
ariaRowIndex: ['*'],
ariaRowSpan: ['*'],
ariaSetSize: ['*'],
};
};

View File

@ -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<string> = new Set<string>();
programEnterVisitor(
path: NodePath<t.Program>,
filename: string | undefined
@ -71,8 +73,19 @@ export class PluginProvider {
programExitVisitor(path: NodePath<t.Program>): 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<t.JSXElement | t.JSXFragment>): 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();

View File

@ -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<Record<string, string[]>>(
(acc, [key, elements]) => {
elements.forEach(element => {
if (!acc[element]) acc[element] = [];
acc[element].push(key);
});
return acc;
},
{}
)
? Object.entries(config.attributeMap).reduce<Record<string, string[]>>((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<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(
Object.entries(props).map(([key, prop]) => {
return [key, this.parseViewProp(prop, generateView)];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<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 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))),
[]
)
)
)];
}
t.callExpression(t.arrowFunctionExpression([], t.blockStatement(statements.concat(returnStatement))), [])
),
[...getUsedApis()],
];
}

View File

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

View File

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

View File

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

View File

@ -58,10 +58,11 @@ describe('HTML', () => {
it('should generate a div element with a static event', () => {
expectView(/*jsx*/`
<div onClick={myFunction}></div>
<div onClick={myFunction} onMouseDown={otherFn}></div>
`, /*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', () => {
</div>
`, /*js*/`
const $node0 = createElement("div")
const $node1 = createComponent(Comp)
const $node1 = runComponent(Comp)
insert($node0, $node1)
`);
});

View File

@ -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] || '');
}, '');
}
}

View File

@ -158,7 +158,7 @@ describe('Template', () => {
</div>
`, /*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*/`

View File

@ -11,4 +11,9 @@ export interface ViewGeneratorConfig {
* @example { href: ["a", "area", "base", "link"], id: ["*"] }
*/
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);
});
}
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., <div>
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