Merge commit '8ecc002f65e0b8f45da1372cf311513dec83993b' into feat/merge-rax-generator

This commit is contained in:
春希 2020-08-19 10:42:12 +08:00
commit 7f5ef2ae57
59 changed files with 2054 additions and 179 deletions

View File

@ -1,9 +1,31 @@
# 出码模块
详细介绍看这里:<https://yuque.antfin-inc.com/docs/share/2b342641-6e01-4c77-b8e0-30421f55f69b>
## 安装接入
## 自定义导出
## 开始开发
本项目隶属于 [ali-lowcode-engine](http://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine), 需要和整个 [ali-lowcode-engine](http://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine) 一起开发。
所以先要初始化整个 [ali-lowcode-engine](http://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine) 的环境:
1. 克隆 [ali-lowcode-engine](http://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine): `git clone git@gitlab.alibaba-inc.com:ali-lowcode/ali-lowcode-engine.git`
2. 运行 `setup` 脚本,初始化环境: `npm run setup`
然后,因为本项目依赖 `@ali/lowcode-types` 所以需要先构建下 `type`,即执行: `lerna run build --scope @ali/lowcode-types`
最后,可以运行 `npm start` 命令启动本地调试(本项目通过 `ava` 进行单元测试,故 `start` 脚本其实就是 `watch` 模式的 `ava`):
```sh
# 到本项目目录下执行:(推荐)
npm start
# 或直接执行 ava
npx ava --watch
# 或在 ali-lowcode-engine 工程根目录下执行: (不推荐,因为命令太长而且没法响应输入)
lerna run start --stream --scope @ali/lowcode-code-generator
```

View File

@ -0,0 +1,5 @@
{
"plugins": [
"build-plugin-component"
]
}

View File

@ -3,13 +3,16 @@
"version": "0.8.10",
"description": "出码引擎 for LowCode Engine",
"main": "lib/index.js",
"module": "es/index.js",
"typings": "es/index.d.ts",
"files": [
"lib",
"es",
"demo"
],
"scripts": {
"start": "ava --watch",
"build": "rimraf lib && tsc",
"build": "rimraf lib && build-scripts build --skip-demo",
"demo": "node ./demo/demo.js",
"test": "ava",
"template": "node ./tools/createTemplate.js"
@ -30,6 +33,7 @@
"short-uuid": "^3.1.1"
},
"devDependencies": {
"@alib/build-scripts": "^0.1.18",
"@types/babel__traverse": "^7.0.10",
"ava": "^1.0.1",
"rimraf": "^3.0.2",

View File

@ -118,8 +118,8 @@ export class ProjectBuilder implements IProjectBuilder {
}
// appConfig
if (parseResult.project && builders.appConfig) {
const { files } = await builders.appConfig.generateModule(parseResult.project);
if (builders.appConfig) {
const { files } = await builders.appConfig.generateModule(parseResult);
buildResult.push({
path: this.template.slots.appConfig.path,

View File

@ -4,6 +4,7 @@
*/
import changeCase from 'change-case';
import { UtilItem, NodeDataType, NodeSchema, ContainerSchema, ProjectSchema, PropsMap } from '@ali/lowcode-types';
import { IPageMeta } from '../types';
import { SUPPORT_SCHEMA_VERSION_LIST } from '../const';
@ -22,6 +23,7 @@ import {
IParseResult,
ISchemaParser,
INpmPackage,
IRouterInfo,
} from '../types';
const defaultContainer: IContainerInfo = {
@ -168,21 +170,21 @@ class SchemaParser implements ISchemaParser {
});
// 分析路由配置
// TODO: 低代码规范里面的路由是咋弄的?
const routes = containers
const routes: IRouterInfo['routes'] = containers
.filter((container) => container.containerType === 'Page')
.map((page) => {
let router = '';
if (page.meta) {
router = (page.meta as any)?.router || '';
}
if (!router) {
router = `/${page.fileName}`;
const meta = page.meta;
if (meta) {
return {
path: (meta as IPageMeta).router || `/${page.fileName}`, // 如果无法找到页面路由信息,则用 fileName 做兜底
fileName: page.fileName,
componentName: page.moduleName,
};
}
return {
path: router,
path: '',
fileName: page.fileName,
componentName: page.moduleName,
};
});

View File

@ -18,7 +18,11 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
type: ChunkType.STRING,
fileType: FileType.JSX,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: `import { createElement, Component } from 'rax';`,
content: `
// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。
// 例外rax 框架的导出名和各种组件名除外。
import { createElement, Component } from 'rax';
`,
linkAfter: [],
});

View File

@ -2,6 +2,9 @@ export const RAX_CHUNK_NAME = {
ClassDidMountBegin: 'RaxComponentClassDidMountBegin',
ClassDidMountContent: 'RaxComponentClassDidMountContent',
ClassDidMountEnd: 'RaxComponentClassDidMountEnd',
ClassWillUnmountBegin: 'RaxComponentClassWillUnmountBegin',
ClassWillUnmountContent: 'RaxComponentClassWillUnmountContent',
ClassWillUnmountEnd: 'RaxComponentClassWillUnmountEnd',
ClassRenderBegin: 'RaxComponentClassRenderBegin',
ClassRenderPre: 'RaxComponentClassRenderPre',
ClassRenderJSX: 'RaxComponentClassRenderJSX',
@ -9,4 +12,7 @@ export const RAX_CHUNK_NAME = {
MethodsBegin: 'RaxComponentMethodsBegin',
MethodsContent: 'RaxComponentMethodsContent',
MethodsEnd: 'RaxComponentMethodsEnd',
LifeCyclesBegin: 'RaxComponentLifeCyclesBegin',
LifeCyclesContent: 'RaxComponentLifeCyclesContent',
LifeCyclesEnd: 'RaxComponentLifeCyclesEnd',
};

View File

@ -84,12 +84,33 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
linkAfter: [RAX_CHUNK_NAME.ClassDidMountBegin, RAX_CHUNK_NAME.ClassDidMountContent],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: RAX_CHUNK_NAME.ClassWillUnmountBegin,
content: `componentWillUnmount() {`,
linkAfter: [
CLASS_DEFINE_CHUNK_NAME.Start,
CLASS_DEFINE_CHUNK_NAME.InsVar,
CLASS_DEFINE_CHUNK_NAME.InsMethod,
RAX_CHUNK_NAME.ClassDidMountEnd,
],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: RAX_CHUNK_NAME.ClassWillUnmountEnd,
content: `}`,
linkAfter: [RAX_CHUNK_NAME.ClassWillUnmountBegin, RAX_CHUNK_NAME.ClassWillUnmountContent],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: RAX_CHUNK_NAME.ClassRenderBegin,
content: 'render() {',
linkAfter: [RAX_CHUNK_NAME.ClassDidMountEnd],
linkAfter: [RAX_CHUNK_NAME.ClassDidMountEnd, RAX_CHUNK_NAME.ClassWillUnmountEnd],
});
next.chunks.push({

View File

@ -53,7 +53,7 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
return self._dataSourceEngine.dataSourceMap || {};
},
async reloadDataSource() {
self._dataSourceEngine.reloadDataSource();
await self._dataSourceEngine.reloadDataSource();
},
get utils() {
return self._utils;
@ -67,6 +67,7 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
get props() {
return self.props;
},
...this._methods,
};
return context;

View File

@ -1,4 +1,4 @@
import { DataSourceConfig } from '@ali/lowcode-types';
import { CompositeValue, DataSourceConfig, isJSExpression, isJSFunction } from '@ali/lowcode-types';
import { CLASS_DEFINE_CHUNK_NAME, COMMON_CHUNK_NAME } from '../../../const/generator';
@ -9,6 +9,9 @@ import {
FileType,
ICodeStruct,
} from '../../../types';
import { generateCompositeType } from '../../../utils/compositeType';
import { parseExpressionConvertThis2Context } from '../../../utils/expressionParser';
import { isContainerSchema } from '../../../utils/schema';
import { RAX_CHUNK_NAME } from './const';
@ -32,7 +35,7 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
fileType: FileType.JSX,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: `
import { createDataSourceEngine } from '@ali/lowcode-datasource-engine';
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
`,
linkAfter: [],
});
@ -42,8 +45,8 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsVar,
content: `
_dataSourceList = this._defineDataSourceList();
_dataSourceEngine = createDataSourceEngine(this._dataSourceList, this._context);`,
_dataSourceConfig = this._defineDataSourceConfig();
_dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this._context, { runtimeConfig: true });`,
linkAfter: [CLASS_DEFINE_CHUNK_NAME.Start],
});
@ -57,18 +60,36 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
linkAfter: [RAX_CHUNK_NAME.ClassDidMountBegin],
});
const dataSource = isContainerSchema(pre.ir) ? pre.ir.dataSource : null;
const dataSourceItems: DataSourceConfig[] = (dataSource && dataSource.list) || [];
const dataSourceConfig = isContainerSchema(pre.ir) ? pre.ir.dataSource : null;
const dataSourceItems: DataSourceConfig[] = (dataSourceConfig && dataSourceConfig.list) || [];
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod,
// TODO: 下面的定义应该需要调用 @ali/lowcode-datasource-engine 的方法来搞:
content: `
_defineDataSourceList() {
return ${JSON.stringify(dataSourceItems)};
}`,
_defineDataSourceConfig() {
const __$$context = this._context;
return (${generateCompositeType(
{
...dataSourceConfig,
list: [
...dataSourceItems.map((item) => ({
...item,
isInit: wrapAsFunction(item.isInit),
options: wrapAsFunction(item.options),
})),
],
},
{
handlers: {
function: (jsFunc) => parseExpressionConvertThis2Context(jsFunc.value, '__$$context'),
expression: (jsExpr) => parseExpressionConvertThis2Context(jsExpr.value, '__$$context'),
},
},
)});
}
`,
linkAfter: [RAX_CHUNK_NAME.ClassRenderEnd],
});
@ -78,3 +99,17 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
};
export default pluginFactory;
function wrapAsFunction(value: CompositeValue): CompositeValue {
if (isJSExpression(value) || isJSFunction(value)) {
return {
type: 'JSExpression',
value: `function(){ return ((${value.value}))}`,
};
}
return {
type: 'JSExpression',
value: `function(){return((${generateCompositeType(value)}))}`,
};
}

View File

@ -1,11 +1,16 @@
// import { JSExpression } from '@ali/lowcode-types';
import _ from 'lodash';
// import { CLASS_DEFINE_CHUNK_NAME, DEFAULT_LINK_AFTER } from '../../../const/generator';
// import { RAX_CHUNK_NAME } from './const';
import { CLASS_DEFINE_CHUNK_NAME } from '../../../const/generator';
import { RAX_CHUNK_NAME } from './const';
// import { getFuncExprBody, transformFuncExpr2MethodMember } from '../../../utils/jsExpression';
import { BuilderComponentPlugin, BuilderComponentPluginFactory, ICodeStruct } from '../../../types';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
FileType,
ChunkType,
ICodeStruct,
IContainerInfo,
} from '../../../types';
type PluginConfig = {
fileType: string;
@ -14,65 +19,98 @@ type PluginConfig = {
};
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
// const cfg: PluginConfig = {
// fileType: FileType.JSX,
// exportNameMapping: {},
// normalizeNameMapping: {},
// ...config,
// };
const cfg: PluginConfig = {
fileType: FileType.JSX,
exportNameMapping: {},
normalizeNameMapping: {},
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
// TODO: Rax 程序的生命周期暂未明确,此处先屏蔽
// @see https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
// Rax 先只支持 didMount 和 willUnmount 吧
// const ir = next.ir as IContainerInfo;
const ir = next.ir as IContainerInfo;
const lifeCycles = ir.lifeCycles;
// if (ir.lifeCycles) {
// const lifeCycles = ir.lifeCycles;
// const chunks = Object.keys(lifeCycles).map<ICodeChunk>(lifeCycleName => {
// const normalizeName = cfg.normalizeNameMapping[lifeCycleName] || lifeCycleName;
// const exportName = cfg.exportNameMapping[lifeCycleName] || lifeCycleName;
// if (normalizeName === 'constructor') {
// return {
// type: ChunkType.STRING,
// fileType: cfg.fileType,
// name: CLASS_DEFINE_CHUNK_NAME.ConstructorContent,
// content: getFuncExprBody(
// (lifeCycles[lifeCycleName] as JSExpression).value,
// ),
// linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.ConstructorStart]],
// };
// }
// if (normalizeName === 'render') {
// return {
// type: ChunkType.STRING,
// fileType: cfg.fileType,
// name: RAX_CHUNK_NAME.ClassRenderPre,
// content: getFuncExprBody(
// (lifeCycles[lifeCycleName] as JSExpression).value,
// ),
// linkAfter: [RAX_CHUNK_NAME.ClassRenderStart],
// };
// }
if (lifeCycles && !_.isEmpty(lifeCycles)) {
Object.entries(lifeCycles).forEach(([lifeCycleName, lifeCycleMethodExpr]) => {
const normalizeName = cfg.normalizeNameMapping[lifeCycleName] || lifeCycleName;
const exportName = cfg.exportNameMapping[lifeCycleName] || lifeCycleName;
// return {
// type: ChunkType.STRING,
// fileType: cfg.fileType,
// name: CLASS_DEFINE_CHUNK_NAME.InsMethod,
// content: transformFuncExpr2MethodMember(
// exportName,
// (lifeCycles[lifeCycleName] as JSExpression).value,
// ),
// linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]],
// };
// });
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.LifeCyclesContent,
content: `${exportName}: (${lifeCycleMethodExpr.value}),`,
linkAfter: [RAX_CHUNK_NAME.LifeCyclesBegin],
});
// next.chunks.push.apply(next.chunks, chunks);
// }
if (normalizeName === 'didMount') {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.ClassDidMountContent,
content: `this._lifeCycles.${exportName}();`,
linkAfter: [RAX_CHUNK_NAME.ClassDidMountBegin],
});
} else if (normalizeName === 'willUnmount') {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.ClassWillUnmountContent,
content: `this._lifeCycles.${exportName}();`,
linkAfter: [RAX_CHUNK_NAME.ClassWillUnmountBegin],
});
} else {
// TODO: print warnings? Unknown life cycle
}
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsVar,
content: `_lifeCycles = this._defineLifeCycles();`,
linkAfter: [CLASS_DEFINE_CHUNK_NAME.Start],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.LifeCyclesBegin,
content: `
_defineLifeCycles() {
const __$$lifeCycles = ({
`,
linkAfter: [RAX_CHUNK_NAME.ClassRenderEnd, CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.LifeCyclesEnd,
content: `
});
// 为所有的方法绑定上下文
Object.entries(__$$lifeCycles).forEach(([lifeCycleName, lifeCycleMethod]) => {
if (typeof lifeCycleMethod === 'function') {
__$$lifeCycles[lifeCycleName] = (...args) => {
return lifeCycleMethod.apply(this._context, args);
}
}
});
return __$$lifeCycles;
}
`,
linkAfter: [RAX_CHUNK_NAME.LifeCyclesBegin, RAX_CHUNK_NAME.LifeCyclesContent],
});
}
return next;
};

View File

@ -44,9 +44,13 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
name: RAX_CHUNK_NAME.MethodsBegin,
content: `
_defineMethods() {
return ({
const __$$methods = ({
`,
linkAfter: [RAX_CHUNK_NAME.ClassRenderEnd, CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod],
linkAfter: [
RAX_CHUNK_NAME.ClassRenderEnd,
CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod,
RAX_CHUNK_NAME.LifeCyclesEnd,
],
});
next.chunks.push({
@ -55,6 +59,17 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
name: RAX_CHUNK_NAME.MethodsEnd,
content: `
});
// 为所有的方法绑定上下文
Object.entries(__$$methods).forEach(([methodName, method]) => {
if (typeof method === 'function') {
__$$methods[methodName] = (...args) => {
return method.apply(this._context, args);
}
}
});
return __$$methods;
}
`,
linkAfter: [RAX_CHUNK_NAME.MethodsBegin, RAX_CHUNK_NAME.MethodsContent],

View File

@ -1,43 +1,58 @@
import { isJSExpression, isJSFunction, JSExpression, JSFunction, NpmInfo } from '@ali/lowcode-types';
import { NodeSchema, JSExpression, NpmInfo, CompositeValue } from '@ali/lowcode-types';
import _ from 'lodash';
import changeCase from 'change-case';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
CodePiece,
FileType,
ICodeChunk,
ICodeStruct,
IContainerInfo,
PIECE_TYPE,
HandlerSet,
} from '../../../types';
import { RAX_CHUNK_NAME } from './const';
import { COMMON_CHUNK_NAME } from '../../../const/generator';
import { createNodeGenerator, generateReactCtrlLine } from '../../../utils/nodeToJSX';
import { generateExpression } from '../../../utils/jsExpression';
import { createNodeGenerator, generateReactCtrlLine, generateAttr } from '../../../utils/nodeToJSX';
import { generateCompositeType } from '../../../utils/compositeType';
import { IScopeBindings, ScopeBindings } from '../../../utils/ScopeBindings';
import { parseExpressionConvertThis2Context, parseExpressionGetGlobalVariables } from '../../../utils/expressionParser';
type PluginConfig = {
fileType: string;
};
// TODO: componentName 若并非大写字符打头,甚至并非是一个有效的 JS 标识符怎么办??
// FIXME: 我想了下,这块应该放到解析阶段就去做掉,对所有 componentName 做 identifier validate然后对不合法的做统一替换。
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
...config,
};
const transformers = {
transformThis2Context,
transformJsExpr: (expr: string) =>
isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${transformThis2Context(expr)}))`,
transformLoopExpr: (expr: string) => `__$$evalArray(() => (${transformThis2Context(expr)}))`,
};
// 什么都不做的的话,会有 3 个问题:
// 1. 小程序出码的时候,循环变量没法拿到
// 2. 小程序出码的时候,很容易出现 Uncaught TypeError: Cannot read property 'avatar' of undefined 这样的异常(如下图的 50 行) -- 因为若直接出码Rax 构建到小程序的时候会立即计算所有在视图中用到的变量
// 3. 通过 this.xxx 能拿到的东西太多了,而且自定义的 methods 可能会无意间破坏 Rax 框架或小程序框架在页面 this 上的东东
// const transformers = {
// transformThis2Context: (expr: string) => expr,
// transformJsExpr: (expr: string) => expr,
// transformLoopExpr: (expr: string) => expr,
// };
const handlers = {
expression: (input: JSExpression) => transformers.transformJsExpr(generateExpression(input)),
function: (input: JSFunction) => transformers.transformJsExpr(input.value || 'null'),
};
// 不转换 this.xxx 到 __$$context.xxx 的话,依然会有上述的 1 和 3 的问题。
// const transformers = {
// transformThis2Context: (expr: string) => expr,
// transformJsExpr: (expr: string) => (isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${expr}))`),
// transformLoopExpr: (expr: string) => `__$$evalArray(() => (${expr}))`,
// };
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
@ -61,19 +76,53 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
// 然后过滤掉所有的别名 chunks
next.chunks = next.chunks.filter((chunk) => !isImportAliasDefineChunk(chunk));
// 创建代码生成器
const generator = createNodeGenerator({
handlers: {
expression: (input: JSExpression) => (isJSExpression(input) ? handlers.expression(input) : ''),
function: (input: JSFunction) => (isJSFunction(input) ? handlers.function(input) : ''),
loopDataExpr: (input: string) => (typeof input === 'string' ? transformers.transformLoopExpr(input) : ''),
tagName: mapComponentNameToAliasOrKeepIt,
const customHandlers: HandlerSet<string> = {
expression(this: CustomHandlerSet, input: JSExpression) {
return transformJsExpr(generateExpression(input), this);
},
function(input) {
return transformThis2Context(input.value || 'null', this);
},
loopDataExpr(input) {
return typeof input === 'string' ? transformLoopExpr(input, this) : '';
},
tagName: mapComponentNameToAliasOrKeepIt,
nodeAttr: generateNodeAttrForRax,
};
// 创建代码生成器
const commonNodeGenerator = createNodeGenerator({
handlers: customHandlers,
plugins: [generateReactCtrlLine],
});
const raxCodeGenerator = (node: NodeSchema): string => {
if (node.loop) {
const loopItemName = node.loopArgs?.[0] || 'item';
const loopIndexName = node.loopArgs?.[1] || 'index';
return runInNewScope({
scopeHost: customHandlers,
newScopeOwnVariables: [loopItemName, loopIndexName],
run: () => commonNodeGenerator(node),
});
}
return commonNodeGenerator(node);
};
customHandlers.node = raxCodeGenerator;
// 生成 JSX 代码
const jsxContent = generator(ir);
const jsxContent = raxCodeGenerator(ir);
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: `import { isMiniApp as __$$isMiniApp } from 'universal-env';`,
linkAfter: [],
});
next.chunks.push({
type: ChunkType.STRING,
@ -122,6 +171,14 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
export default pluginFactory;
function transformLoopExpr(expr: string, handlers: CustomHandlerSet) {
return `__$$evalArray(() => (${transformThis2Context(expr, handlers)}))`;
}
function transformJsExpr(expr: string, handlers: CustomHandlerSet) {
return isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${transformThis2Context(expr, handlers)}))`;
}
function isImportAliasDefineChunk(
chunk: ICodeChunk,
): chunk is ICodeChunk & {
@ -151,11 +208,97 @@ function isLiteralAtomicExpr(expr: string): boolean {
* this.xxx __$$context.xxx
* @param expr
*/
function transformThis2Context(expr: string): string {
// TODO: 应该根据语法分析来搞
// TODO: 如何替换自定义名字的循环变量generateReactCtrlLine
return expr
.replace(/\bthis\.item\./, () => 'item.')
.replace(/\bthis\.index\./, () => 'index.')
.replace(/\bthis\./, () => '__$$context.');
function transformThis2Context(expr: string, customHandlers: CustomHandlerSet): string {
// return expr
// .replace(/\bthis\.item\./g, () => 'item.')
// .replace(/\bthis\.index\./g, () => 'index.')
// .replace(/\bthis\./g, () => '__$$context.');
return parseExpressionConvertThis2Context(expr, '__$$context', customHandlers.scopeBindings?.getAllBindings() || []);
}
function generateNodeAttrForRax(this: CustomHandlerSet, attrName: string, attrValue: CompositeValue): CodePiece[] {
if (!/^on/.test(attrName)) {
return generateAttr(attrName, attrValue, {
...this,
nodeAttr: undefined,
});
}
// 先出个码
const valueExpr = generateCompositeType(attrValue, this);
// 查询当前作用域下的变量
const currentScopeVariables = this.scopeBindings?.getAllBindings() || [];
if (currentScopeVariables.length <= 0) {
return [
{
type: PIECE_TYPE.ATTR,
value: `${attrName}={${valueExpr}}`,
},
];
}
// 提取出所有的未定义的全局变量
const undeclaredVariablesInValueExpr = parseExpressionGetGlobalVariables(valueExpr);
const referencedLocalVariables = _.intersection(undeclaredVariablesInValueExpr, currentScopeVariables);
if (referencedLocalVariables.length <= 0) {
return [
{
type: PIECE_TYPE.ATTR,
value: `${attrName}={${valueExpr}}`,
},
];
}
const wrappedAttrValueExpr = [
`(...__$$args) => {`,
` if (__$$isMiniApp) {`,
` const __$$event = __$$args[0];`,
...referencedLocalVariables.map((localVar) => `const ${localVar} = __$$event.target.dataset.${localVar};`),
` return (${valueExpr}).apply(this, __$$args);`,
` } else {`,
` return (${valueExpr}).apply(this, __$$args);`,
` }`,
`}`,
].join('\n');
return [
...referencedLocalVariables.map((localVar) => ({
type: PIECE_TYPE.ATTR,
value: `data-${changeCase.snake(localVar)}={${localVar}}`,
})),
{
type: PIECE_TYPE.ATTR,
value: `${attrName}={${wrappedAttrValueExpr}}`,
},
];
}
function runInNewScope<T>({
scopeHost,
newScopeOwnVariables,
run,
}: {
scopeHost: {
scopeBindings?: IScopeBindings;
};
newScopeOwnVariables: string[];
run: () => T;
}): T {
const originalScopeBindings = scopeHost.scopeBindings;
try {
const newScope = new ScopeBindings(originalScopeBindings);
newScopeOwnVariables.forEach((varName) => {
newScope.addBinding(varName);
});
scopeHost.scopeBindings = newScope;
return run();
} finally {
scopeHost.scopeBindings = originalScopeBindings;
}
}

View File

@ -1,3 +1,4 @@
import changeCase from 'change-case';
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
@ -6,7 +7,7 @@ import {
ChunkType,
FileType,
ICodeStruct,
IRouterInfo,
IParseResult,
} from '../../../../../types';
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
@ -15,23 +16,22 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
...pre,
};
const ir = next.ir as IRouterInfo;
const ir = next.ir as IParseResult;
const routes = ir.globalRouter?.routes?.map((route) => ({
path: route.path,
source: `pages/${changeCase.pascalCase(route.fileName)}/index`,
})) || [{ path: '/', source: 'pages/Home/index' }];
// TODO: 如何生成路由?
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSON,
name: COMMON_CHUNK_NAME.CustomContent,
content: `
{
"routes": [
{
"path": "/",
"source": "pages/Home/index"
}
],
"routes": ${JSON.stringify(routes, null, 2)},
"window": {
"title": "Rax App Demo"
"title": ${JSON.stringify(ir.project?.meta?.title || ir.project?.meta?.name || '')}
}
}
`,

View File

@ -24,6 +24,8 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
content: `
import { runApp } from 'rax-app';
import appConfig from './app.json';
import './global.scss';
`,
linkAfter: [],
});

View File

@ -21,9 +21,7 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
type: ChunkType.STRING,
fileType: FileType.SCSS, // TODO: 样式文件的类型定制化?
name: COMMON_CHUNK_NAME.StyleDepsImport,
content: `
// TODO: 引入默认全局样式
`,
content: ``,
linkAfter: [],
});

View File

@ -35,6 +35,7 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
},
dependencies: {
'@ali/lowcode-datasource-engine': '^0.1.0',
'universal-env': '^3.2.0',
rax: '^1.1.0',
'rax-app': '^2.0.0',
'rax-document': '^0.1.0',

View File

@ -60,8 +60,6 @@ export default function createIceJsProjectBuilder(): IProjectBuilder {
htmlEntry: [raxApp.plugins.entryDocument()],
packageJSON: [raxApp.plugins.packageJSON()],
},
postProcessors: [
// prettier() // 暂且禁用 prettier
],
postProcessors: process.env.NODE_ENV !== 'test' ? [prettier()] : [],
});
}

View File

@ -1,4 +1,9 @@
import {
JSONArray,
JSONObject,
CompositeValue,
CompositeArray,
CompositeObject,
ResultDir,
ResultFile,
NodeDataType,
@ -145,6 +150,8 @@ export interface CodePiece {
type: PIECE_TYPE;
}
// FIXME: 在新的实现中,添加了第一参数 this: CustomHandlerSet 作为上下文。究其本质
// scopeBindings?: IScopeBindings;
export interface HandlerSet<T> {
string?: (input: string) => T;
boolean?: (input: boolean) => T;
@ -153,13 +160,15 @@ export interface HandlerSet<T> {
function?: (input: JSFunction) => T;
slot?: (input: JSSlot) => T;
node?: (input: NodeSchema) => T;
array?: (input: any[]) => T;
array?: (input: JSONArray | CompositeArray) => T;
children?: (input: T[]) => T;
object?: (input: object) => T;
object?: (input: JSONObject | CompositeObject) => T;
common?: (input: unknown) => T;
tagName?: (input: string) => T;
loopDataExpr?: (input: string) => T;
conditionExpr?: (input: string) => T;
nodeAttrs?(node: NodeSchema): CodePiece[];
nodeAttr?(attrName: string, attrValue: CompositeValue): CodePiece[];
}
export type ExtGeneratorPlugin = (ctx: INodeGeneratorContext, nodeItem: NodeSchema) => CodePiece[];

View File

@ -26,6 +26,7 @@ export interface IUtilInfo extends IWithDependency {
export interface IRouterInfo extends IWithDependency {
routes: Array<{
path: string;
fileName: string;
componentName: string;
}>;
}
@ -39,9 +40,14 @@ export interface IProjectInfo {
packages: INpmPackage[];
meta?: {
name?: string;
title?: string;
};
}
export interface IPageMeta {
router?: string;
}
/**
* From meta
* page title

View File

@ -0,0 +1,33 @@
export class OrderedSet<T> {
private _set = new Set<T>();
private _arr: T[] = [];
constructor(items?: T[]) {
if (items) {
this._set = new Set(items);
this._arr = items.slice(0);
}
}
add(item: T) {
if (!this._set.has(item)) {
this._set.add(item);
this._arr.push(item);
}
}
delete(item: T) {
if (this._set.has(item)) {
this._set.delete(item);
this._arr.splice(this._arr.indexOf(item), 1);
}
}
has(item: T) {
return this._set.has(item);
}
toArray() {
return this._arr.slice(0);
}
}

View File

@ -0,0 +1,52 @@
import { OrderedSet } from './OrderedSet';
export interface IScopeBindings {
readonly parent: IScopeBindings | null;
hasBinding(varName: string): boolean;
hasOwnBinding(varName: string): boolean;
addBinding(varName: string): void;
removeBinding(varName: string): void;
getAllBindings(): string[];
getAllOwnedBindings(): string[];
}
export class ScopeBindings implements IScopeBindings {
private _bindings = new OrderedSet<string>();
constructor(readonly parent: IScopeBindings | null = null) {}
hasBinding(varName: string): boolean {
return this._bindings.has(varName) || !!this.parent?.hasBinding(varName);
}
hasOwnBinding(varName: string): boolean {
return this._bindings.has(varName);
}
addBinding(varName: string): void {
this._bindings.add(varName);
}
removeBinding(varName: string): void {
this._bindings.delete(varName);
}
getAllBindings(): string[] {
const allBindings = new OrderedSet(this._bindings.toArray());
for (let parent = this.parent; parent; parent = parent?.parent) {
parent.getAllOwnedBindings().forEach((varName) => {
allBindings.add(varName);
});
}
return allBindings.toArray();
}
getAllOwnedBindings(): string[] {
return this._bindings.toArray();
}
}

View File

@ -8,13 +8,12 @@ import {
} from '@ali/lowcode-types';
import _ from 'lodash';
import { CompositeValueGeneratorOptions, CodeGeneratorError } from '../types';
import { generateExpression, generateFunction } from './jsExpression';
import { generateJsSlot } from './jsSlot';
import { isValidIdentifier } from './validate';
import { camelize } from './common';
import { CompositeValueGeneratorOptions, CodeGeneratorError } from '../types';
function generateArray(value: CompositeArray, options: CompositeValueGeneratorOptions = {}): string {
const body = value.map((v) => generateUnknownType(v, options)).join(',');
return `[${body}]`;

View File

@ -0,0 +1,239 @@
import * as parser from '@babel/parser';
import generate from '@babel/generator';
import traverse, { NodePath } from '@babel/traverse';
import * as t from '@babel/types';
import { isIdentifier, Node } from '@babel/types';
import { OrderedSet } from './OrderedSet';
export class ParseError extends Error {
constructor(public readonly expr: string, public readonly detail: unknown) {
super(`Failed to parse expression "${expr}"`);
Object.setPrototypeOf(this, new.target.prototype);
}
}
const MAYBE_EXPRESSIONS: {
[k in Node['type']]?: {
// fields: Array<keyof (Node & { type: k })>
fields: string[] | ((node: Node) => string[]);
};
} = {
ArrayExpression: { fields: ['elements'] },
AssignmentExpression: { fields: ['left', 'right'] },
BinaryExpression: { fields: ['left', 'right'] },
CallExpression: { fields: ['arguments', 'callee'] },
ConditionalExpression: { fields: ['test', 'consequent', 'alternate'] },
DoWhileStatement: { fields: ['test'] },
ExpressionStatement: { fields: ['expression'] },
ForInStatement: { fields: ['right'] },
ForStatement: { fields: ['init', 'test', 'update'] },
IfStatement: { fields: ['test'] },
LogicalExpression: { fields: ['left', 'right'] },
MemberExpression: {
fields: (node) => (node.type === 'MemberExpression' && node.computed ? ['object', 'property'] : ['object']),
},
NewExpression: { fields: ['callee', 'arguments'] },
ObjectMethod: {
fields: (node) => (node.type === 'ObjectMethod' && node.computed ? ['key'] : []),
},
ObjectProperty: {
fields: (node) => (node.type === 'ObjectProperty' && node.computed ? ['key', 'value'] : ['value']),
},
ReturnStatement: { fields: ['argument'] },
SequenceExpression: { fields: ['expressions'] },
ParenthesizedExpression: { fields: ['expression'] },
SwitchCase: { fields: ['test'] },
SwitchStatement: { fields: ['discriminant'] },
ThrowStatement: { fields: ['argument'] },
UnaryExpression: { fields: ['argument'] },
UpdateExpression: { fields: ['argument'] },
VariableDeclarator: { fields: ['init'] },
WhileStatement: { fields: ['test'] },
WithStatement: { fields: ['object'] },
AssignmentPattern: { fields: ['right'] },
ArrowFunctionExpression: { fields: ['body'] },
ClassExpression: { fields: ['superClass'] },
ClassDeclaration: { fields: ['superClass'] },
ExportDefaultDeclaration: { fields: ['declaration'] },
ForOfStatement: { fields: ['right'] },
ClassMethod: { fields: (node) => (node.type === 'ClassMethod' && node.computed ? ['key'] : []) },
SpreadElement: { fields: ['argument'] },
TaggedTemplateExpression: { fields: ['tag'] },
TemplateLiteral: { fields: ['expressions'] },
YieldExpression: { fields: ['argument'] },
AwaitExpression: { fields: ['argument'] },
OptionalMemberExpression: {
fields: (node) => (node.type === 'OptionalMemberExpression' && node.computed ? ['object', 'property'] : ['object']),
},
OptionalCallExpression: { fields: ['callee', 'arguments'] },
JSXSpreadAttribute: { fields: ['argument'] },
BindExpression: { fields: ['object', 'callee'] },
ClassProperty: { fields: (node) => (node.type === 'ClassProperty' && node.computed ? ['key', 'value'] : ['value']) },
PipelineTopicExpression: { fields: ['expression'] },
PipelineBareFunction: { fields: ['callee'] },
ClassPrivateProperty: { fields: ['value'] },
Decorator: { fields: ['expression'] },
TupleExpression: { fields: ['elements'] },
TSDeclareMethod: { fields: (node) => (node.type === 'TSDeclareMethod' && node.computed ? ['key'] : []) },
TSPropertySignature: {
fields: (node) => (node.type === 'TSPropertySignature' && node.computed ? ['key', 'initializer'] : ['initializer']),
},
TSMethodSignature: {
fields: (node) => (node.type === 'TSMethodSignature' && node.computed ? ['key'] : []),
},
TSAsExpression: { fields: ['expression'] },
TSTypeAssertion: { fields: ['expression'] },
TSEnumDeclaration: { fields: ['initializer'] },
TSEnumMember: { fields: ['initializer'] },
TSNonNullExpression: { fields: ['expression'] },
TSExportAssignment: { fields: ['expression'] },
};
export type ParseExpressionGetGlobalVariablesOptions = { filter?: (varName: string) => boolean };
const CROSS_THIS_SCOPE_TYPE_NODE: {
[k in Node['type']]?: boolean;
} = {
ArrowFunctionExpression: false, // 箭头函数不跨越 this 的 scope
FunctionExpression: true,
FunctionDeclaration: true,
// FunctionTypeAnnotation: false, // 这是 TS 定义
// FunctionTypeParam: false, // 这是 TS 定义
ClassDeclaration: true,
ClassExpression: true,
ClassBody: true,
ClassImplements: true,
ClassMethod: true,
ClassPrivateMethod: true,
ClassProperty: true,
ClassPrivateProperty: true,
DeclareClass: true,
};
export function parseExpressionGetGlobalVariables(
expr: string | null | undefined,
{ filter = (x) => true }: ParseExpressionGetGlobalVariablesOptions = {},
): string[] {
if (!expr) {
return [];
}
try {
const undeclaredVars = new OrderedSet<string>();
const ast = parser.parse(`!(${expr});`);
const addUndeclaredIdentifierIfNeeded = (x: object | null | undefined, path: NodePath<Node>) => {
if (isIdentifier(x) && !path.scope.hasBinding(x.name)) {
undeclaredVars.add(x.name);
}
};
traverse(ast, {
enter(path) {
const node = path.node;
const expressionFields = MAYBE_EXPRESSIONS[node.type]?.fields;
if (expressionFields) {
(typeof expressionFields === 'function' ? expressionFields(node) : expressionFields).forEach((fieldName) => {
const fieldValue = node[fieldName as keyof typeof node];
if (typeof fieldValue === 'object') {
if (Array.isArray(fieldValue)) {
fieldValue.forEach((item) => {
addUndeclaredIdentifierIfNeeded(item, path);
});
} else {
addUndeclaredIdentifierIfNeeded(fieldValue, path);
}
}
});
}
},
});
return undeclaredVars.toArray().filter(filter);
} catch (e) {
throw new ParseError(expr, e);
}
}
export function parseExpressionConvertThis2Context(
expr: string,
contextName: string = '__$$context',
localVariables: string[] = [],
): string {
if (!expr) {
return expr;
}
try {
const exprAst = parser.parseExpression(expr);
const exprWrapAst = t.expressionStatement(exprAst);
const fileAst = t.file(t.program([exprWrapAst]));
const localVariablesSet = new Set(localVariables);
let thisScopeLevel = CROSS_THIS_SCOPE_TYPE_NODE[exprAst.type] ? -1 : 0;
traverse(fileAst, {
enter(path) {
if (CROSS_THIS_SCOPE_TYPE_NODE[path.node.type]) {
thisScopeLevel++;
}
},
exit(path) {
if (CROSS_THIS_SCOPE_TYPE_NODE[path.node.type]) {
thisScopeLevel--;
}
},
MemberExpression(path) {
if (!path.isMemberExpression()) {
return;
}
const obj = path.get('object');
if (!obj.isThisExpression()) {
return;
}
// 处理局部变量
if (!path.node.computed) {
const prop = path.get('property');
if (prop.isIdentifier() && localVariablesSet.has(prop.node.name)) {
path.replaceWith(t.identifier(prop.node.name));
return;
}
}
// 替换 this (只在顶层替换)
if (thisScopeLevel <= 0) {
obj.replaceWith(t.identifier(contextName));
}
},
ThisExpression(path) {
if (!path.isThisExpression()) {
return;
}
// MemberExpression 中的 this.xxx 已经处理过了
if (path.parent.type === 'MemberExpression') {
return;
}
if (thisScopeLevel <= 0) {
path.replaceWith(t.identifier(contextName));
}
},
});
const { code } = generate(exprWrapAst.expression, { sourceMaps: false });
return code;
} catch (e) {
// throw new ParseError(expr, e);
throw e;
}
}
function indent(level: number) {
return ' '.repeat(level);
}

View File

@ -86,13 +86,15 @@ export function handleSubNodes<T>(
}
export function generateAttr(ctx: INodeGeneratorContext, attrName: string, attrValue: any): CodePiece[] {
if (attrName === 'initValue') {
return [];
if (ctx.handlers.nodeAttr) {
return ctx.handlers.nodeAttr(attrName, attrValue);
}
const valueStr = generateCompositeType(attrValue, {
containerHandler: (v, isStr, vStr) => (isStr ? `"${vStr}"` : `{${v}}`),
nodeGenerator: ctx.generator,
});
return [
{
value: `${attrName}=${valueStr}`,
@ -102,6 +104,10 @@ export function generateAttr(ctx: INodeGeneratorContext, attrName: string, attrV
}
export function generateAttrs(ctx: INodeGeneratorContext, nodeItem: NodeSchema): CodePiece[] {
if (ctx.handlers.nodeAttrs) {
return ctx.handlers.nodeAttrs(nodeItem);
}
const { props } = nodeItem;
let pieces: CodePiece[] = [];
@ -118,6 +124,7 @@ export function generateAttrs(ctx: INodeGeneratorContext, nodeItem: NodeSchema):
}
// TODO: 处理 spread 场景(<Xxx {...(something)}/>)
// 这种在 schema 里面怎么描述
});
}
}
@ -214,9 +221,11 @@ export function generateReactCtrlLine(ctx: INodeGeneratorContext, nodeItem: Node
const loopItemName = nodeItem.loopArgs?.[0] || 'item';
const loopIndexName = nodeItem.loopArgs?.[1] || 'index';
const loopDataExpr = (ctx.handlers.loopDataExpr || _.identity)(
isJSExpression(nodeItem.loop) ? `(${nodeItem.loop.value})` : `(${JSON.stringify(nodeItem.loop)})`,
);
const rawLoopDataExpr = isJSExpression(nodeItem.loop)
? `(${nodeItem.loop.value})`
: `(${JSON.stringify(nodeItem.loop)})`;
const loopDataExpr = ctx.handlers.loopDataExpr ? ctx.handlers.loopDataExpr(rawLoopDataExpr) : rawLoopDataExpr;
pieces.unshift({
value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => (`,

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@ali/lowcode-datasource-engine": "^0.1.0",
"universal-env": "^3.2.0",
"rax": "^1.1.0",
"rax-app": "^2.0.0",
"rax-document": "^0.1.0",

View File

@ -1,4 +1,6 @@
import { runApp } from 'rax-app';
import appConfig from './app.json';
import './global.scss';
runApp(appConfig);

View File

@ -1,5 +1,3 @@
// TODO: 引入默认全局样式
body {
-webkit-font-smoothing: antialiased;
}

View File

@ -1,10 +1,14 @@
// : "__$$" 访
// rax
import { createElement, Component } from 'rax';
import Page from 'rax-view';
import Text from 'rax-text';
import { createDataSourceEngine } from '@ali/lowcode-datasource-engine';
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
import { isMiniApp as __$$isMiniApp } from 'universal-env';
import __$$projectUtils from '../../utils';
@ -15,8 +19,8 @@ class Home$$Page extends Component {
_context = this._createContext();
_dataSourceList = this._defineDataSourceList();
_dataSourceEngine = createDataSourceEngine(this._dataSourceList, this._context);
_dataSourceConfig = this._defineDataSourceConfig();
_dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this._context, { runtimeConfig: true });
_utils = this._defineUtils();
@ -24,6 +28,8 @@ class Home$$Page extends Component {
this._dataSourceEngine.reloadDataSource();
}
componentWillUnmount() {}
render() {
const __$$context = this._context;
@ -48,7 +54,7 @@ class Home$$Page extends Component {
return self._dataSourceEngine.dataSourceMap || {};
},
async reloadDataSource() {
self._dataSourceEngine.reloadDataSource();
await self._dataSourceEngine.reloadDataSource();
},
get utils() {
return self._utils;
@ -62,13 +68,15 @@ class Home$$Page extends Component {
get props() {
return self.props;
},
...this._methods,
};
return context;
}
_defineDataSourceList() {
return [];
_defineDataSourceConfig() {
const __$$context = this._context;
return { list: [] };
}
_defineUtils() {
@ -86,7 +94,18 @@ class Home$$Page extends Component {
}
_defineMethods() {
return {};
const __$$methods = {};
//
Object.entries(__$$methods).forEach(([methodName, method]) => {
if (typeof method === 'function') {
__$$methods[methodName] = (...args) => {
return method.apply(this._context, args);
};
}
});
return __$$methods;
}
}

View File

@ -23,6 +23,9 @@
props: {},
lifeCycles: {},
fileName: 'home',
meta: {
router: '/',
},
dataSource: {
list: [],
},

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@ali/lowcode-datasource-engine": "^0.1.0",
"universal-env": "^3.2.0",
"rax": "^1.1.0",
"rax-app": "^2.0.0",
"rax-document": "^0.1.0",
@ -17,7 +18,8 @@
"rax-text": "^1.0.0",
"rax-image": "^1.0.0",
"moment": "*",
"lodash": "*"
"lodash": "*",
"universal-toast": "^1.2.0"
},
"devDependencies": {
"build-plugin-rax-app": "^5.0.0",

View File

@ -1,4 +1,6 @@
import { runApp } from 'rax-app';
import appConfig from './app.json';
import './global.scss';
runApp(appConfig);

View File

@ -1,5 +1,9 @@
// TODO: 引入默认全局样式
body {
-webkit-font-smoothing: antialiased;
}
page,
body {
width: 750rpx;
overflow-x: hidden;
}

View File

@ -1,3 +1,5 @@
// : "__$$" 访
// rax
import { createElement, Component } from 'rax';
import View from 'rax-view';
@ -6,7 +8,9 @@ import Text from 'rax-text';
import Image from 'rax-image';
import { createDataSourceEngine } from '@ali/lowcode-datasource-engine';
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
import { isMiniApp as __$$isMiniApp } from 'universal-env';
import __$$projectUtils from '../../utils';
@ -14,6 +18,7 @@ import './index.css';
class Home$$Page extends Component {
state = {
clickCount: 0,
user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' },
orders: [
{
@ -30,14 +35,24 @@ class Home$$Page extends Component {
};
_methods = this._defineMethods();
_context = this._createContext();
_dataSourceList = this._defineDataSourceList();
_dataSourceEngine = createDataSourceEngine(this._dataSourceList, this._context);
_dataSourceConfig = this._defineDataSourceConfig();
_dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this._context, { runtimeConfig: true });
_utils = this._defineUtils();
_lifeCycles = this._defineLifeCycles();
componentDidMount() {
this._dataSourceEngine.reloadDataSource();
this._lifeCycles.didMount();
}
componentWillUnmount() {
this._lifeCycles.willUnmount();
}
render() {
@ -57,7 +72,7 @@ class Home$$Page extends Component {
source={{ uri: __$$eval(() => __$$context.state.user.avatar) }}
style={{ width: '32px', height: '32px' }}
/>
<View>
<View onClick={__$$context.hello}>
<Text>{__$$eval(() => __$$context.state.user.name)}</Text>
<Text>{__$$eval(() => __$$context.state.user.age)}</Text>
</View>
@ -66,17 +81,48 @@ class Home$$Page extends Component {
<View>
<Text>=== Orders: ===</Text>
</View>
{__$$evalArray(() => __$$context.state.orders).map((item, index) => (
<View style={{ flexDirection: 'row' }}>
{__$$evalArray(() => __$$context.state.orders).map((order, index) => (
<View
style={{ flexDirection: 'row' }}
data-order={order}
onClick={(...__$$args) => {
if (__$$isMiniApp) {
const __$$event = __$$args[0];
const order = __$$event.target.dataset.order;
return function () {
__$$context.utils.recordEvent(`CLICK_ORDER`, order.title);
}.apply(this, __$$args);
} else {
return function () {
__$$context.utils.recordEvent(`CLICK_ORDER`, order.title);
}.apply(this, __$$args);
}
}}
>
<View>
<Image source={{ uri: __$$eval(() => item.coverUrl) }} style={{ width: '80px', height: '60px' }} />
<Image source={{ uri: __$$eval(() => order.coverUrl) }} style={{ width: '80px', height: '60px' }} />
</View>
<View>
<Text>{__$$eval(() => item.title)}</Text>
<Text>{__$$eval(() => __$$context.utils.formatPrice(item.price, '元'))}</Text>
<Text>{__$$eval(() => order.title)}</Text>
<Text>{__$$eval(() => __$$context.utils.formatPrice(order.price, '元'))}</Text>
</View>
</View>
))}
<View
onClick={function () {
__$$context.setState({
clickCount: __$$context.state.clickCount + 1,
});
}}
>
<Text>点击次数{__$$eval(() => __$$context.state.clickCount)}(点击加 1)</Text>
</View>
<View>
<Text>操作提示</Text>
<Text>1. 点击会员名可以弹出 Toast "Hello xxx!"</Text>
<Text>2. 点击订单会记录点击的订单信息并弹出 Toast 提示</Text>
<Text>3. 最下面的点击次数点一次应该加 1</Text>
</View>
</View>
);
}
@ -95,7 +141,7 @@ class Home$$Page extends Component {
return self._dataSourceEngine.dataSourceMap || {};
},
async reloadDataSource() {
self._dataSourceEngine.reloadDataSource();
await self._dataSourceEngine.reloadDataSource();
},
get utils() {
return self._utils;
@ -109,21 +155,71 @@ class Home$$Page extends Component {
get props() {
return self.props;
},
...this._methods,
};
return context;
}
_defineDataSourceList() {
return [
{ id: 'urlParams', type: 'urlParams' },
{ id: 'user', type: 'fetch', options: { method: 'GET', uri: 'https://shs.alibaba-inc.com/mock/1458/demo/user' } },
{
id: 'orders',
type: 'fetch',
options: { method: 'GET', uri: 'https://shs.alibaba-inc.com/mock/1458/demo/orders' },
_defineDataSourceConfig() {
const __$$context = this._context;
return {
list: [
{
id: 'urlParams',
type: 'urlParams',
isInit: function () {
return undefined;
},
options: function () {
return undefined;
},
},
{
id: 'user',
type: 'fetch',
options: function () {
return {
method: 'GET',
uri: 'https://shs.alibaba-inc.com/mock/1458/demo/user',
};
},
dataHandler: function (response) {
if (!response.success) {
throw new Error(response.message);
}
return response.data;
},
isInit: function () {
return undefined;
},
},
{
id: 'orders',
type: 'fetch',
options: function () {
return {
method: 'GET',
uri: __$$context.state.user.ordersApiUri,
};
},
dataHandler: function (response) {
if (!response.success) {
throw new Error(response.message);
}
return response.data.result;
},
isInit: function () {
return undefined;
},
},
],
dataHandler: function (dataMap) {
console.info('All datasources loaded:', dataMap);
},
];
};
}
_defineUtils() {
@ -140,12 +236,47 @@ class Home$$Page extends Component {
return utils;
}
_defineMethods() {
return {
hello: function hello() {
console.log('Hello world!');
_defineLifeCycles() {
const __$$lifeCycles = {
didMount: function didMount() {
this.utils.Toast.show(`Hello ${this.state.user.name}!`);
},
willUnmount: function didMount() {
this.utils.Toast.show(`Bye, ${this.state.user.name}!`);
},
};
//
Object.entries(__$$lifeCycles).forEach(([lifeCycleName, lifeCycleMethod]) => {
if (typeof lifeCycleMethod === 'function') {
__$$lifeCycles[lifeCycleName] = (...args) => {
return lifeCycleMethod.apply(this._context, args);
};
}
});
return __$$lifeCycles;
}
_defineMethods() {
const __$$methods = {
hello: function hello() {
this.utils.Toast.show(`Hello ${this.state.user.name}!`);
console.log(`Hello ${this.state.user.name}!`);
},
};
//
Object.entries(__$$methods).forEach(([methodName, method]) => {
if (typeof method === 'function') {
__$$methods[methodName] = (...args) => {
return method.apply(this._context, args);
};
}
});
return __$$methods;
}
}

View File

@ -2,14 +2,25 @@ import moment from 'moment';
import clone from 'lodash/clone';
import Toast from 'universal-toast';
const formatPrice = function formatPrice(price, unit) {
return Number(price).toFixed(2) + unit;
};
const recordEvent = function recordEvent(eventName, eventDetail) {
this.utils.Toast.show(`[EVENT]: ${eventName} ${eventDetail}`);
console.log(`[EVENT]: ${eventName} (detail: %o) (user: %o)`, eventDetail, this.state.user);
};
export default {
formatPrice,
recordEvent,
moment,
clone,
Toast,
};

View File

@ -35,7 +35,11 @@
{
componentName: 'Page',
fileName: 'home',
meta: {
router: '/',
},
state: {
clickCount: 0,
user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' },
orders: [
{
@ -51,11 +55,20 @@
],
},
props: {},
lifeCycles: {},
lifeCycles: {
didMount: {
type: 'JSExpression',
value: 'function didMount(){\n this.utils.Toast.show(`Hello ${this.state.user.name}!`);\n}',
},
willUnmount: {
type: 'JSExpression',
value: 'function didMount(){\n this.utils.Toast.show(`Bye, ${this.state.user.name}!`);\n}',
},
},
methods: {
hello: {
type: 'JSExpression',
value: 'function hello(){ console.log("Hello world!"); }',
value: 'function hello(){\n this.utils.Toast.show(`Hello ${this.state.user.name}!`);\n console.log(`Hello ${this.state.user.name}!`); }',
},
},
dataSource: {
@ -72,6 +85,10 @@
method: 'GET',
uri: 'https://shs.alibaba-inc.com/mock/1458/demo/user',
},
dataHandler: {
type: 'JSFunction',
value: 'function (response) {\nif (!response.success){\n throw new Error(response.message);\n }\n return response.data;\n}',
},
},
// 示例数据源https://shs.alibaba-inc.com/mock/1458/demo/orders
{
@ -79,10 +96,21 @@
type: 'fetch',
options: {
method: 'GET',
uri: 'https://shs.alibaba-inc.com/mock/1458/demo/orders',
uri: {
type: 'JSExpression',
value: 'this.state.user.ordersApiUri',
},
},
dataHandler: {
type: 'JSFunction',
value: 'function (response) {\nif (!response.success){\n throw new Error(response.message);\n }\n return response.data.result;\n}',
},
},
],
dataHandler: {
type: 'JSFunction',
value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}',
},
},
children: [
{
@ -132,6 +160,12 @@
},
{
componentName: 'View',
props: {
onClick: {
type: 'JSFunction',
value: 'this.hello',
},
},
children: [
{
componentName: 'Text',
@ -170,8 +204,13 @@
type: 'JSExpression',
value: 'this.state.orders',
},
loopArgs: ['order', 'index'],
props: {
style: { flexDirection: 'row' },
onClick: {
type: 'JSFunction',
value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.order.title) }',
},
},
children: [
{
@ -183,7 +222,7 @@
source: {
uri: {
type: 'JSExpression',
value: 'this.item.coverUrl',
value: 'this.order.coverUrl',
},
},
style: {
@ -201,24 +240,72 @@
componentName: 'Text',
children: {
type: 'JSExpression',
value: 'this.item.title',
value: 'this.order.title',
},
},
{
componentName: 'Text',
children: {
type: 'JSExpression',
value: 'this.utils.formatPrice(this.item.price, "元")',
value: 'this.utils.formatPrice(this.order.price, "元")',
},
},
],
},
],
},
{
componentName: 'View',
props: {
onClick: {
type: 'JSFunction',
value: 'function (){ this.setState({ clickCount: this.state.clickCount + 1 }) }',
},
},
children: [
{
componentName: 'Text',
children: [
'点击次数:',
{
type: 'JSExpression',
value: 'this.state.clickCount',
},
'(点击加 1)',
],
},
],
},
{
componentName: 'View',
children: [
{
componentName: 'Text',
props: {},
children: '操作提示:',
},
{
componentName: 'Text',
props: {},
children: '1. 点击会员名,可以弹出 Toast "Hello xxx!"',
},
{
componentName: 'Text',
props: {},
children: '2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示',
},
{
componentName: 'Text',
props: {},
children: '3. 最下面的【点击次数】,点一次应该加 1',
},
],
},
],
},
],
utils: [
// 可以直接定义一个函数
{
name: 'formatPrice',
type: 'function',
@ -227,6 +314,16 @@
value: 'function formatPrice(price, unit) { return Number(price).toFixed(2) + unit; }',
},
},
// 在 utils 里面也可以用 this 访问当前上下文:
{
name: 'recordEvent',
type: 'function',
content: {
type: 'JSFunction',
value: 'function recordEvent(eventName, eventDetail) { \n this.utils.Toast.show(`[EVENT]: ${eventName} ${eventDetail}`);\n console.log(`[EVENT]: ${eventName} (detail: %o) (user: %o)`, eventDetail, this.state.user); }',
},
},
// 也可以直接从 npm 包引入 (下例等价于 `import moment from 'moment';`)
{
name: 'moment',
type: 'npm',
@ -236,6 +333,7 @@
exportName: 'moment',
},
},
// 可以引入子目录(下例等价于 `import clone from 'lodash/clone';`)
{
name: 'clone',
type: 'npm',
@ -247,7 +345,18 @@
main: '/clone',
},
},
// 支持 TNPM
{
name: 'Toast',
type: 'tnpm',
content: {
package: 'universal-toast',
version: '^1.2.0',
exportName: 'Toast', // TODO: 这个 exportName 是否可以省略?省略后默认是上一层的 name
},
},
],
css: 'page,body{\n width: 750rpx;\n overflow-x: hidden;\n}',
config: {
sdkVersion: '1.0.3',
historyMode: 'hash',

View File

@ -0,0 +1,12 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,11 @@
# 忽略目录
build/
tests/
demo/
# node 覆盖率文件
coverage/
# 忽略文件
**/*-min.js
**/*.min.js

View File

@ -0,0 +1,3 @@
module.exports = {
extends: ['rax'],
};

View File

@ -0,0 +1,17 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
*~
*.swp
*.log
.DS_Store
.idea/
.temp/
build/
dist/
lib/
coverage/
node_modules/
template.yml

View File

@ -0,0 +1,15 @@
# @ali/rax-component-demo
## Getting Started
### `npm run start`
Runs the app in development mode.
Open [http://localhost:9999](http://localhost:9999) to view it in the browser.
The page will reload if you make edits.
### `npm run build`
Builds the app for production to the `build` folder.

View File

@ -0,0 +1,7 @@
{
"type": "rax",
"builder": "@ali/builder-rax-v1",
"info": {
"raxVersion": "1.x"
}
}

View File

@ -0,0 +1,12 @@
{
"inlineStyle": false,
"plugins": [
[
"build-plugin-rax-app",
{
"targets": ["web", "miniapp"]
}
],
"@ali/build-plugin-rax-app-def"
]
}

View File

@ -0,0 +1,35 @@
{
"name": "@ali/rax-app-demo",
"private": true,
"version": "1.0.0",
"scripts": {
"build": "rm -f ./dist/miniapp.tar.gz && npm run build:miniapp && cd build/miniapp && tar czf ../../dist/miniapp.tar.gz *",
"build:miniapp": "build-scripts build",
"start": "build-scripts start",
"lint": "eslint --ext .js --ext .jsx ./"
},
"dependencies": {
"@ali/lowcode-datasource-engine": "^0.1.0",
"universal-env": "^3.2.0",
"rax": "^1.1.0",
"rax-app": "^2.0.0",
"rax-document": "^0.1.0",
"rax-view": "^1.0.0",
"rax-text": "^1.0.0",
"rax-link": "^1.0.0",
"rax-image": "^1.0.0"
},
"devDependencies": {
"build-plugin-rax-app": "^5.0.0",
"@alib/build-scripts": "^0.1.0",
"@typescript-eslint/eslint-plugin": "^2.11.0",
"@typescript-eslint/parser": "^2.11.0",
"babel-eslint": "^10.0.3",
"eslint": "^6.8.0",
"eslint-config-rax": "^0.1.0",
"eslint-plugin-import": "^2.20.0",
"eslint-plugin-module": "^0.1.0",
"eslint-plugin-react": "^7.18.0",
"@ali/build-plugin-rax-app-def": "^1.0.0"
}
}

View File

@ -0,0 +1,6 @@
import { runApp } from 'rax-app';
import appConfig from './app.json';
import './global.scss';
runApp(appConfig);

View File

@ -0,0 +1,19 @@
{
"routes": [
{
"path": "/",
"source": "pages/Home/index"
},
{
"path": "/list",
"source": "pages/List/index"
},
{
"path": "/detail",
"source": "pages/Detail/index"
}
],
"window": {
"title": "Rax App Demo"
}
}

View File

@ -0,0 +1,25 @@
import { createElement } from 'rax';
import { Root, Style, Script } from 'rax-document';
function Document() {
return (
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,viewport-fit=cover"
/>
<title>Rax App Demo</title>
<Style />
</head>
<body>
{/* root container */}
<Root />
<Script />
</body>
</html>
);
}
export default Document;

View File

@ -0,0 +1,9 @@
body {
-webkit-font-smoothing: antialiased;
}
page,
body {
width: 750rpx;
overflow-x: hidden;
}

View File

@ -0,0 +1,136 @@
// : "__$$" 访
// rax
import { createElement, Component } from 'rax';
import View from 'rax-view';
import Text from 'rax-text';
import Link from 'rax-link';
import Image from 'rax-image';
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
import { isMiniApp as __$$isMiniApp } from 'universal-env';
import __$$projectUtils from '../../utils';
import './index.css';
class Detail$$Page extends Component {
state = {};
_methods = this._defineMethods();
_context = this._createContext();
_dataSourceConfig = this._defineDataSourceConfig();
_dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this._context, { runtimeConfig: true });
_utils = this._defineUtils();
componentDidMount() {
this._dataSourceEngine.reloadDataSource();
}
componentWillUnmount() {}
render() {
const __$$context = this._context;
return (
<View>
<View>
<Text>This is the Detail Page</Text>
</View>
<Link href="javascript:history.back();" miniappHref="navigateBack:">
<Text>Go back</Text>
</Link>
</View>
);
}
_createContext() {
const self = this;
const context = {
get state() {
return self.state;
},
setState(newState) {
self.setState(newState);
},
get dataSourceMap() {
return self._dataSourceEngine.dataSourceMap || {};
},
async reloadDataSource() {
await self._dataSourceEngine.reloadDataSource();
},
get utils() {
return self._utils;
},
get page() {
return context;
},
get component() {
return context;
},
get props() {
return self.props;
},
...this._methods,
};
return context;
}
_defineDataSourceConfig() {
const __$$context = this._context;
return { list: [] };
}
_defineUtils() {
const utils = {
...__$$projectUtils,
};
Object.entries(utils).forEach(([name, util]) => {
if (typeof util === 'function') {
utils[name] = util.bind(this._context);
}
});
return utils;
}
_defineMethods() {
const __$$methods = {};
//
Object.entries(__$$methods).forEach(([methodName, method]) => {
if (typeof method === 'function') {
__$$methods[methodName] = (...args) => {
return method.apply(this._context, args);
};
}
});
return __$$methods;
}
}
export default Detail$$Page;
function __$$eval(expr) {
try {
return expr();
} catch (err) {
console.warn('Failed to evaluate: ', expr, err);
}
}
function __$$evalArray(expr) {
const res = __$$eval(expr);
return Array.isArray(res) ? res : [];
}

View File

@ -0,0 +1,136 @@
// : "__$$" 访
// rax
import { createElement, Component } from 'rax';
import View from 'rax-view';
import Text from 'rax-text';
import Link from 'rax-link';
import Image from 'rax-image';
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
import { isMiniApp as __$$isMiniApp } from 'universal-env';
import __$$projectUtils from '../../utils';
import './index.css';
class Home$$Page extends Component {
state = {};
_methods = this._defineMethods();
_context = this._createContext();
_dataSourceConfig = this._defineDataSourceConfig();
_dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this._context, { runtimeConfig: true });
_utils = this._defineUtils();
componentDidMount() {
this._dataSourceEngine.reloadDataSource();
}
componentWillUnmount() {}
render() {
const __$$context = this._context;
return (
<View>
<View>
<Text>This is the Home Page</Text>
</View>
<Link href="#/list" miniappHref="navigate:/pages/List/index">
<Text>Go To The List Page</Text>
</Link>
</View>
);
}
_createContext() {
const self = this;
const context = {
get state() {
return self.state;
},
setState(newState) {
self.setState(newState);
},
get dataSourceMap() {
return self._dataSourceEngine.dataSourceMap || {};
},
async reloadDataSource() {
await self._dataSourceEngine.reloadDataSource();
},
get utils() {
return self._utils;
},
get page() {
return context;
},
get component() {
return context;
},
get props() {
return self.props;
},
...this._methods,
};
return context;
}
_defineDataSourceConfig() {
const __$$context = this._context;
return { list: [] };
}
_defineUtils() {
const utils = {
...__$$projectUtils,
};
Object.entries(utils).forEach(([name, util]) => {
if (typeof util === 'function') {
utils[name] = util.bind(this._context);
}
});
return utils;
}
_defineMethods() {
const __$$methods = {};
//
Object.entries(__$$methods).forEach(([methodName, method]) => {
if (typeof method === 'function') {
__$$methods[methodName] = (...args) => {
return method.apply(this._context, args);
};
}
});
return __$$methods;
}
}
export default Home$$Page;
function __$$eval(expr) {
try {
return expr();
} catch (err) {
console.warn('Failed to evaluate: ', expr, err);
}
}
function __$$evalArray(expr) {
const res = __$$eval(expr);
return Array.isArray(res) ? res : [];
}

View File

@ -0,0 +1,139 @@
// : "__$$" 访
// rax
import { createElement, Component } from 'rax';
import View from 'rax-view';
import Text from 'rax-text';
import Link from 'rax-link';
import Image from 'rax-image';
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
import { isMiniApp as __$$isMiniApp } from 'universal-env';
import __$$projectUtils from '../../utils';
import './index.css';
class List$$Page extends Component {
state = {};
_methods = this._defineMethods();
_context = this._createContext();
_dataSourceConfig = this._defineDataSourceConfig();
_dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this._context, { runtimeConfig: true });
_utils = this._defineUtils();
componentDidMount() {
this._dataSourceEngine.reloadDataSource();
}
componentWillUnmount() {}
render() {
const __$$context = this._context;
return (
<View>
<View>
<Text>This is the List Page</Text>
</View>
<Link href="#/detail" miniappHref="navigate:/pages/Detail/index">
<Text>Go To The Detail Page</Text>
</Link>
<Link href="javascript:history.back();" miniappHref="navigateBack:">
<Text>Go back</Text>
</Link>
</View>
);
}
_createContext() {
const self = this;
const context = {
get state() {
return self.state;
},
setState(newState) {
self.setState(newState);
},
get dataSourceMap() {
return self._dataSourceEngine.dataSourceMap || {};
},
async reloadDataSource() {
await self._dataSourceEngine.reloadDataSource();
},
get utils() {
return self._utils;
},
get page() {
return context;
},
get component() {
return context;
},
get props() {
return self.props;
},
...this._methods,
};
return context;
}
_defineDataSourceConfig() {
const __$$context = this._context;
return { list: [] };
}
_defineUtils() {
const utils = {
...__$$projectUtils,
};
Object.entries(utils).forEach(([name, util]) => {
if (typeof util === 'function') {
utils[name] = util.bind(this._context);
}
});
return utils;
}
_defineMethods() {
const __$$methods = {};
//
Object.entries(__$$methods).forEach(([methodName, method]) => {
if (typeof method === 'function') {
__$$methods[methodName] = (...args) => {
return method.apply(this._context, args);
};
}
});
return __$$methods;
}
}
export default List$$Page;
function __$$eval(expr) {
try {
return expr();
} catch (err) {
console.warn('Failed to evaluate: ', expr, err);
}
}
function __$$evalArray(expr) {
const res = __$$eval(expr);
return Array.isArray(res) ? res : [];
}

View File

@ -0,0 +1 @@
export default {};

View File

@ -0,0 +1,175 @@
{
// Schema 参见https://yuque.antfin-inc.com/mo/spec/spec-materials#eNCJr
version: '1.0.0',
componentsMap: [
{
componentName: 'View',
package: 'rax-view',
version: '^1.0.0',
destructuring: false,
exportName: 'View',
},
{
componentName: 'Text',
package: 'rax-text',
version: '^1.0.0',
destructuring: false,
exportName: 'Text',
},
{
componentName: 'Link',
package: 'rax-link',
version: '^1.0.0',
destructuring: false,
exportName: 'Link',
},
{
componentName: 'Image',
package: 'rax-image',
version: '^1.0.0',
destructuring: false,
exportName: 'Image',
},
{
componentName: 'Page',
package: 'rax-view',
version: '^1.0.0',
destructuring: false,
exportName: 'Page',
},
],
componentsTree: [
{
componentName: 'Page',
fileName: 'home',
state: {},
dataSource: {
list: [],
},
meta: {
router: '/',
},
children: [
{
componentName: 'View',
children: [
{
componentName: 'Text',
children: 'This is the Home Page',
},
],
},
{
componentName: 'Link',
props: {
href: '#/list',
miniappHref: 'navigate:/pages/List/index',
},
children: [
{
componentName: 'Text',
children: 'Go To The List Page',
},
],
},
],
},
{
componentName: 'Page',
fileName: 'list',
state: {},
dataSource: {
list: [],
},
meta: {
router: '/list',
},
children: [
{
componentName: 'View',
children: [
{
componentName: 'Text',
children: 'This is the List Page',
},
],
},
{
componentName: 'Link',
props: {
href: '#/detail',
miniappHref: 'navigate:/pages/Detail/index',
},
children: [
{
componentName: 'Text',
children: 'Go To The Detail Page',
},
],
},
{
componentName: 'Link',
props: {
href: 'javascript:history.back();',
miniappHref: 'navigateBack:',
},
children: [
{
componentName: 'Text',
children: 'Go back',
},
],
},
],
},
{
componentName: 'Page',
fileName: 'detail',
state: {},
dataSource: {
list: [],
},
meta: {
router: '/detail',
},
children: [
{
componentName: 'View',
children: [
{
componentName: 'Text',
children: 'This is the Detail Page',
},
],
},
{
componentName: 'Link',
props: {
href: 'javascript:history.back();',
miniappHref: 'navigateBack:',
},
children: [
{
componentName: 'Text',
children: 'Go back',
},
],
},
],
},
],
css: 'page,body{\n width: 750rpx;\n overflow-x: hidden;\n}',
config: {
sdkVersion: '1.0.3',
historyMode: 'hash',
targetRootID: 'root',
},
meta: {
name: 'Rax App Demo',
git_group: 'demo-group',
project_name: 'demo-project',
description: '这是一个示例应用',
spma: 'spmademo',
creator: '张三',
},
}

View File

@ -0,0 +1,79 @@
import test from 'ava';
import type { ExecutionContext, Macro } from 'ava';
import { parseExpressionConvertThis2Context } from '../../../src/utils/expressionParser';
const macro: Macro<any[]> = (
t: ExecutionContext<{}>,
input: [string, string, string[]],
expected: string,
error?: { message: RegExp },
) => {
if (!error) {
t.deepEqual(parseExpressionConvertThis2Context(input[0], input[1], input[2]), expected);
} else {
t.throws(() => {
t.deepEqual(parseExpressionConvertThis2Context(input[0], input[1], input[2]), expected);
}, error.message);
}
};
macro.title = (providedTitle: string | undefined, ...args: any[]): string => {
const [input, expected] = args;
return providedTitle || `after convert this to context "${input[0]}" should be "${expected}"`.replace(/\n|\s+/g, ' ');
};
test(macro, ['this.hello', '__$$context', []], '__$$context.hello');
test(macro, ['this.utils.recordEvent', '__$$context', []], '__$$context.utils.recordEvent');
test(
macro,
['this.utils.recordEvent.bind(this)', '__$$context', []],
'__$$context.utils.recordEvent.bind(__$$context)',
);
test(macro, ['this.item', '__$$context', ['item']], 'item');
test(macro, ['this.user.name', '__$$context', ['user']], 'user.name');
test(macro, ['function (){}', '__$$context', []], 'function () {}');
test(
macro,
['function (){ this.utils.Toast.show("Hello world!") }', '__$$context'],
'function () {\n __$$context.utils.Toast.show("Hello world!");\n}',
);
// 变量能被替换掉
test(
macro,
['function (){ this.utils.recordEvent("click", this.item) }', '__$$context', ['item']],
'function () {\n __$$context.utils.recordEvent("click", item);\n}',
);
// 只替换顶层的,不替换内层
test(
macro,
['function (){ return function (){ this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']],
'function () {\n return function () {\n this.utils.recordEvent("click", item);\n };\n}',
);
// 只替换顶层的,不替换内层
test(
macro,
['function onClick(){ return function (){ this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']],
'function onClick() {\n return function () {\n this.utils.recordEvent("click", item);\n };\n}',
);
// 只替换顶层的,不替换内层
test(
macro,
['() => { return function (){ this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']],
'() => {\n return function () {\n this.utils.recordEvent("click", item);\n };\n}',
);
// 但是若内层有用箭头函数定义的则还是要替换下
test(
macro,
['() => { return () => { this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']],
'() => {\n return () => {\n __$$context.utils.recordEvent("click", item);\n };\n}',
);

View File

@ -0,0 +1,114 @@
import test from 'ava';
import type { ExecutionContext, Macro } from 'ava';
import {
parseExpressionGetGlobalVariables,
ParseExpressionGetGlobalVariablesOptions,
} from '../../../src/utils/expressionParser';
const macro: Macro<any[]> = (
t: ExecutionContext<{}>,
input: [string | null | undefined, ParseExpressionGetGlobalVariablesOptions],
expected: string[],
error?: { message: RegExp },
) => {
if (!error) {
t.deepEqual(parseExpressionGetGlobalVariables(input[0], input[1]), expected);
} else {
t.throws(() => {
t.deepEqual(parseExpressionGetGlobalVariables(input[0], input[1]), expected);
}, error.message);
}
};
macro.title = (providedTitle: string | undefined, ...args: any[]): string => {
const [input, expected] = args;
return providedTitle || `global variables of "${input[0]}" should be "${expected.join(', ')}"`;
};
test(macro, ['function (){ }', {}], []);
test(macro, ['function (){ __$$context.utils.Toast.show("Hello world!") }', {}], ['__$$context']);
test(macro, ['function (){ __$$context.utils.formatPrice(item.price1, "元") }', {}], ['__$$context', 'item']);
test(
macro,
[
'function (){ __$$context.utils.formatPrice(item2, "元"); }',
{ filter: (varName: string) => !/^__\$\$/.test(varName) },
],
['item2'],
);
test(
macro,
[
'function (){ __$$context.utils.log(item3, [item4, item5]); }',
{ filter: (varName: string) => !/^__\$\$/.test(varName) },
],
['item3', 'item4', 'item5'],
);
test(
macro,
['function (){ item3[item4]("Hello"); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3', 'item4'],
);
test(macro, ['function (){ item3("Hello"); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], ['item3']);
test(
macro,
['function foo(){ foo[item3]("Hello"); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3'],
);
// isAssignmentExpression/right
test(
macro,
['function (){ let foo; foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3'],
);
// isAssignmentExpression/left
test(
macro,
['function (){ foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['foo', 'item3'],
);
// isVariableDeclarator
test(
macro,
['function (){ const foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3'],
);
// isVariableDeclarator
test(
macro,
['function (){ let foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3'],
);
// isVariableDeclarator
test(
macro,
['function (){ var foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3'],
);
// isTemplateLiteral
test(
macro,
['function (){ console.log(`Hello ${item3};`); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['console', 'item3'],
);
// isBinaryExpression
test(
macro,
['function (){ console.log(item2 | item3); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['console', 'item2', 'item3'],
);
// TODO: 补充更多类型的测试用例

View File

@ -10,7 +10,7 @@ export interface DataSourceConfig {
type: string;
requestHandler?: JSFunction;
dataHandler?: JSFunction;
options: {
options?: {
uri: string | JSExpression;
params?: JSONObject | JSExpression;
method?: string | JSExpression;