!159 fix(transpiler): auto import
* fix(transpiler): auto import * feat(transpiler): auto import
This commit is contained in:
parent
2640177de5
commit
2445208856
|
@ -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",
|
||||
|
|
|
@ -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';
|
|
@ -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();
|
||||
|
|
|
@ -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';
|
|
@ -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>"');
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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: ['*'],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)];
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
)
|
||||
])];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
|||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
|||
)
|
||||
])];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ export const importMap = [
|
|||
'addEventListener',
|
||||
'watch',
|
||||
'insert',
|
||||
'createComponent',
|
||||
'runComponent',
|
||||
'createText'
|
||||
].reduce<Record<string, string>>((acc, cur) => {
|
||||
acc[cur] = cur;
|
||||
|
|
|
@ -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;
|
||||
};`
|
||||
})();`
|
||||
]
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -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] || '');
|
||||
}, '');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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*/`
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue