diff --git a/packages/code-generator/README.md b/packages/code-generator/README.md index fe71e6c6a..35b2d9aba 100644 --- a/packages/code-generator/README.md +++ b/packages/code-generator/README.md @@ -1,9 +1,31 @@ # 出码模块 +详细介绍看这里: + ## 安装接入 - ## 自定义导出 - ## 开始开发 + +本项目隶属于 [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 +``` diff --git a/packages/code-generator/build.json b/packages/code-generator/build.json new file mode 100644 index 000000000..bd5cf18dd --- /dev/null +++ b/packages/code-generator/build.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "build-plugin-component" + ] +} diff --git a/packages/code-generator/package.json b/packages/code-generator/package.json index 3f4af4a45..dca06c932 100644 --- a/packages/code-generator/package.json +++ b/packages/code-generator/package.json @@ -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", diff --git a/packages/code-generator/src/generator/ProjectBuilder.ts b/packages/code-generator/src/generator/ProjectBuilder.ts index cb3ec8bd5..870e96855 100644 --- a/packages/code-generator/src/generator/ProjectBuilder.ts +++ b/packages/code-generator/src/generator/ProjectBuilder.ts @@ -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, diff --git a/packages/code-generator/src/parser/SchemaParser.ts b/packages/code-generator/src/parser/SchemaParser.ts index 910f810c0..426479383 100644 --- a/packages/code-generator/src/parser/SchemaParser.ts +++ b/packages/code-generator/src/parser/SchemaParser.ts @@ -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, }; }); diff --git a/packages/code-generator/src/plugins/component/rax/commonDeps.ts b/packages/code-generator/src/plugins/component/rax/commonDeps.ts index 49f1a8d2f..4d827fc8a 100644 --- a/packages/code-generator/src/plugins/component/rax/commonDeps.ts +++ b/packages/code-generator/src/plugins/component/rax/commonDeps.ts @@ -18,7 +18,11 @@ const pluginFactory: BuilderComponentPluginFactory = () => { 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: [], }); diff --git a/packages/code-generator/src/plugins/component/rax/const.ts b/packages/code-generator/src/plugins/component/rax/const.ts index d604b27e9..a0a07b550 100644 --- a/packages/code-generator/src/plugins/component/rax/const.ts +++ b/packages/code-generator/src/plugins/component/rax/const.ts @@ -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', }; diff --git a/packages/code-generator/src/plugins/component/rax/containerClass.ts b/packages/code-generator/src/plugins/component/rax/containerClass.ts index 4ec4b6245..0db104861 100644 --- a/packages/code-generator/src/plugins/component/rax/containerClass.ts +++ b/packages/code-generator/src/plugins/component/rax/containerClass.ts @@ -84,12 +84,33 @@ const pluginFactory: BuilderComponentPluginFactory = () => { 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({ diff --git a/packages/code-generator/src/plugins/component/rax/containerInjectContext.ts b/packages/code-generator/src/plugins/component/rax/containerInjectContext.ts index f5bfe65cc..228d7dad5 100644 --- a/packages/code-generator/src/plugins/component/rax/containerInjectContext.ts +++ b/packages/code-generator/src/plugins/component/rax/containerInjectContext.ts @@ -53,7 +53,7 @@ const pluginFactory: BuilderComponentPluginFactory = (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 = (config?) => get props() { return self.props; }, + ...this._methods, }; return context; diff --git a/packages/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts b/packages/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts index 019383755..417e1f45d 100644 --- a/packages/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts +++ b/packages/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts @@ -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 = (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 = (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 = (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 = (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)}))}`, + }; +} diff --git a/packages/code-generator/src/plugins/component/rax/containerLifeCycle.ts b/packages/code-generator/src/plugins/component/rax/containerLifeCycle.ts index 18119a572..348d6679f 100644 --- a/packages/code-generator/src/plugins/component/rax/containerLifeCycle.ts +++ b/packages/code-generator/src/plugins/component/rax/containerLifeCycle.ts @@ -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 = (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(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; }; diff --git a/packages/code-generator/src/plugins/component/rax/containerMethods.ts b/packages/code-generator/src/plugins/component/rax/containerMethods.ts index 833831bda..042462456 100644 --- a/packages/code-generator/src/plugins/component/rax/containerMethods.ts +++ b/packages/code-generator/src/plugins/component/rax/containerMethods.ts @@ -44,9 +44,13 @@ const pluginFactory: BuilderComponentPluginFactory = (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 = (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], diff --git a/packages/code-generator/src/plugins/component/rax/jsx.ts b/packages/code-generator/src/plugins/component/rax/jsx.ts index 85fdfe05a..93edbf6da 100644 --- a/packages/code-generator/src/plugins/component/rax/jsx.ts +++ b/packages/code-generator/src/plugins/component/rax/jsx.ts @@ -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 = (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 = (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 = { + 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 = (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({ + 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; + } } diff --git a/packages/code-generator/src/plugins/project/framework/rax/plugins/appConfig.ts b/packages/code-generator/src/plugins/project/framework/rax/plugins/appConfig.ts index 2d630c407..354e5f93c 100644 --- a/packages/code-generator/src/plugins/project/framework/rax/plugins/appConfig.ts +++ b/packages/code-generator/src/plugins/project/framework/rax/plugins/appConfig.ts @@ -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 = () => { @@ -15,23 +16,22 @@ const pluginFactory: BuilderComponentPluginFactory = () => { ...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 || '')} } } `, diff --git a/packages/code-generator/src/plugins/project/framework/rax/plugins/entry.ts b/packages/code-generator/src/plugins/project/framework/rax/plugins/entry.ts index e586ac2e2..b838f8abe 100644 --- a/packages/code-generator/src/plugins/project/framework/rax/plugins/entry.ts +++ b/packages/code-generator/src/plugins/project/framework/rax/plugins/entry.ts @@ -24,6 +24,8 @@ const pluginFactory: BuilderComponentPluginFactory = () => { content: ` import { runApp } from 'rax-app'; import appConfig from './app.json'; + +import './global.scss'; `, linkAfter: [], }); diff --git a/packages/code-generator/src/plugins/project/framework/rax/plugins/globalStyle.ts b/packages/code-generator/src/plugins/project/framework/rax/plugins/globalStyle.ts index a990947a9..78e409fd2 100644 --- a/packages/code-generator/src/plugins/project/framework/rax/plugins/globalStyle.ts +++ b/packages/code-generator/src/plugins/project/framework/rax/plugins/globalStyle.ts @@ -21,9 +21,7 @@ const pluginFactory: BuilderComponentPluginFactory = () => { type: ChunkType.STRING, fileType: FileType.SCSS, // TODO: 样式文件的类型定制化? name: COMMON_CHUNK_NAME.StyleDepsImport, - content: ` -// TODO: 引入默认全局样式 -`, + content: ``, linkAfter: [], }); diff --git a/packages/code-generator/src/plugins/project/framework/rax/plugins/packageJSON.ts b/packages/code-generator/src/plugins/project/framework/rax/plugins/packageJSON.ts index cbdb2237e..fd0c8246f 100644 --- a/packages/code-generator/src/plugins/project/framework/rax/plugins/packageJSON.ts +++ b/packages/code-generator/src/plugins/project/framework/rax/plugins/packageJSON.ts @@ -35,6 +35,7 @@ const pluginFactory: BuilderComponentPluginFactory = () => { }, 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', diff --git a/packages/code-generator/src/solutions/rax-app.ts b/packages/code-generator/src/solutions/rax-app.ts index 11a215c5c..2c6864393 100644 --- a/packages/code-generator/src/solutions/rax-app.ts +++ b/packages/code-generator/src/solutions/rax-app.ts @@ -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()] : [], }); } diff --git a/packages/code-generator/src/types/core.ts b/packages/code-generator/src/types/core.ts index 8c4dd9458..93d069a9e 100644 --- a/packages/code-generator/src/types/core.ts +++ b/packages/code-generator/src/types/core.ts @@ -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 { string?: (input: string) => T; boolean?: (input: boolean) => T; @@ -153,13 +160,15 @@ export interface HandlerSet { 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[]; diff --git a/packages/code-generator/src/types/intermediate.ts b/packages/code-generator/src/types/intermediate.ts index d3f77867f..11e6cc7b3 100644 --- a/packages/code-generator/src/types/intermediate.ts +++ b/packages/code-generator/src/types/intermediate.ts @@ -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 diff --git a/packages/code-generator/src/utils/OrderedSet.ts b/packages/code-generator/src/utils/OrderedSet.ts new file mode 100644 index 000000000..3c58ddc73 --- /dev/null +++ b/packages/code-generator/src/utils/OrderedSet.ts @@ -0,0 +1,33 @@ +export class OrderedSet { + private _set = new Set(); + 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); + } +} diff --git a/packages/code-generator/src/utils/ScopeBindings.ts b/packages/code-generator/src/utils/ScopeBindings.ts new file mode 100644 index 000000000..f68b0ff42 --- /dev/null +++ b/packages/code-generator/src/utils/ScopeBindings.ts @@ -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(); + + 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(); + } +} diff --git a/packages/code-generator/src/utils/compositeType.ts b/packages/code-generator/src/utils/compositeType.ts index dab597c3b..8471e240f 100644 --- a/packages/code-generator/src/utils/compositeType.ts +++ b/packages/code-generator/src/utils/compositeType.ts @@ -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}]`; diff --git a/packages/code-generator/src/utils/expressionParser.ts b/packages/code-generator/src/utils/expressionParser.ts new file mode 100644 index 000000000..0b7755171 --- /dev/null +++ b/packages/code-generator/src/utils/expressionParser.ts @@ -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 + 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(); + + const ast = parser.parse(`!(${expr});`); + + const addUndeclaredIdentifierIfNeeded = (x: object | null | undefined, path: NodePath) => { + 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); +} diff --git a/packages/code-generator/src/utils/nodeToJSX.ts b/packages/code-generator/src/utils/nodeToJSX.ts index 30a333c6b..3f454d076 100644 --- a/packages/code-generator/src/utils/nodeToJSX.ts +++ b/packages/code-generator/src/utils/nodeToJSX.ts @@ -86,13 +86,15 @@ export function handleSubNodes( } 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 场景() + // 这种在 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}) => (`, diff --git a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/package.json b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/package.json index f2ad5aa15..6d3ae68a5 100644 --- a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/package.json +++ b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/package.json @@ -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", diff --git a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/app.js b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/app.js index f04a4233b..bc474c6e3 100644 --- a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/app.js +++ b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/app.js @@ -1,4 +1,6 @@ import { runApp } from 'rax-app'; import appConfig from './app.json'; +import './global.scss'; + runApp(appConfig); diff --git a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/global.scss b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/global.scss index 5a853d5a9..0d203c62a 100644 --- a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/global.scss +++ b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/global.scss @@ -1,5 +1,3 @@ -// TODO: 引入默认全局样式 - body { -webkit-font-smoothing: antialiased; } diff --git a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/pages/Home/index.jsx b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/pages/Home/index.jsx index 1ae8e9192..53c839182 100644 --- a/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/pages/Home/index.jsx +++ b/packages/code-generator/test-cases/rax-app/demo1/expected/demo-project/src/pages/Home/index.jsx @@ -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; } } diff --git a/packages/code-generator/test-cases/rax-app/demo1/schema.json5 b/packages/code-generator/test-cases/rax-app/demo1/schema.json5 index 510b70006..1d405aee8 100644 --- a/packages/code-generator/test-cases/rax-app/demo1/schema.json5 +++ b/packages/code-generator/test-cases/rax-app/demo1/schema.json5 @@ -23,6 +23,9 @@ props: {}, lifeCycles: {}, fileName: 'home', + meta: { + router: '/', + }, dataSource: { list: [], }, diff --git a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/package.json b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/package.json index 8aad3e1ad..893e55365 100644 --- a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/package.json +++ b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/package.json @@ -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", diff --git a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/app.js b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/app.js index f04a4233b..bc474c6e3 100644 --- a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/app.js +++ b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/app.js @@ -1,4 +1,6 @@ import { runApp } from 'rax-app'; import appConfig from './app.json'; +import './global.scss'; + runApp(appConfig); diff --git a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/global.scss b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/global.scss index 5a853d5a9..2b98f3726 100644 --- a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/global.scss +++ b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/global.scss @@ -1,5 +1,9 @@ -// TODO: 引入默认全局样式 - body { -webkit-font-smoothing: antialiased; } + +page, +body { + width: 750rpx; + overflow-x: hidden; +} diff --git a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/pages/Home/index.jsx b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/pages/Home/index.jsx index 1f1e8cfea..970e1987d 100644 --- a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/pages/Home/index.jsx +++ b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/pages/Home/index.jsx @@ -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' }} /> - + {__$$eval(() => __$$context.state.user.name)} {__$$eval(() => __$$context.state.user.age)}岁 @@ -66,17 +81,48 @@ class Home$$Page extends Component { === Orders: === - {__$$evalArray(() => __$$context.state.orders).map((item, index) => ( - + {__$$evalArray(() => __$$context.state.orders).map((order, index) => ( + { + 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); + } + }} + > - item.coverUrl) }} style={{ width: '80px', height: '60px' }} /> + order.coverUrl) }} style={{ width: '80px', height: '60px' }} /> - {__$$eval(() => item.title)} - {__$$eval(() => __$$context.utils.formatPrice(item.price, '元'))} + {__$$eval(() => order.title)} + {__$$eval(() => __$$context.utils.formatPrice(order.price, '元'))} ))} + + 点击次数:{__$$eval(() => __$$context.state.clickCount)}(点击加 1) + + + 操作提示: + 1. 点击会员名,可以弹出 Toast "Hello xxx!" + 2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示 + 3. 最下面的【点击次数】,点一次应该加 1 + ); } @@ -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; } } diff --git a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/utils.js b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/utils.js index 6fb1f681b..a9612b3d2 100644 --- a/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/utils.js +++ b/packages/code-generator/test-cases/rax-app/demo2/expected/demo-project/src/utils.js @@ -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, }; diff --git a/packages/code-generator/test-cases/rax-app/demo2/schema.json5 b/packages/code-generator/test-cases/rax-app/demo2/schema.json5 index 161d7fe50..5154efdea 100644 --- a/packages/code-generator/test-cases/rax-app/demo2/schema.json5 +++ b/packages/code-generator/test-cases/rax-app/demo2/schema.json5 @@ -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', diff --git a/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.editorconfig b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.editorconfig new file mode 100644 index 000000000..5760be583 --- /dev/null +++ b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.editorconfig @@ -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 diff --git a/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.eslintignore b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.eslintignore new file mode 100644 index 000000000..3b437e614 --- /dev/null +++ b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.eslintignore @@ -0,0 +1,11 @@ +# 忽略目录 +build/ +tests/ +demo/ + +# node 覆盖率文件 +coverage/ + +# 忽略文件 +**/*-min.js +**/*.min.js diff --git a/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.eslintrc.js b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.eslintrc.js new file mode 100644 index 000000000..e2a7c5b54 --- /dev/null +++ b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['rax'], +}; diff --git a/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.gitignore b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.gitignore new file mode 100644 index 000000000..50a53dace --- /dev/null +++ b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/.gitignore @@ -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 diff --git a/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/README.md b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/README.md new file mode 100644 index 000000000..6eff85d41 --- /dev/null +++ b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/README.md @@ -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. diff --git a/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/abc.json b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/abc.json new file mode 100644 index 000000000..f9ee40f71 --- /dev/null +++ b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/abc.json @@ -0,0 +1,7 @@ +{ + "type": "rax", + "builder": "@ali/builder-rax-v1", + "info": { + "raxVersion": "1.x" + } +} diff --git a/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/build.json b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/build.json new file mode 100644 index 000000000..f3e9b9323 --- /dev/null +++ b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/build.json @@ -0,0 +1,12 @@ +{ + "inlineStyle": false, + "plugins": [ + [ + "build-plugin-rax-app", + { + "targets": ["web", "miniapp"] + } + ], + "@ali/build-plugin-rax-app-def" + ] +} diff --git a/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/package.json b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/package.json new file mode 100644 index 000000000..7d41d4764 --- /dev/null +++ b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/package.json @@ -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" + } +} diff --git a/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/src/app.js b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/src/app.js new file mode 100644 index 000000000..bc474c6e3 --- /dev/null +++ b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/src/app.js @@ -0,0 +1,6 @@ +import { runApp } from 'rax-app'; +import appConfig from './app.json'; + +import './global.scss'; + +runApp(appConfig); diff --git a/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/src/app.json b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/src/app.json new file mode 100644 index 000000000..5082c4f9d --- /dev/null +++ b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/src/app.json @@ -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" + } +} diff --git a/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/src/document/index.jsx b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/src/document/index.jsx new file mode 100644 index 000000000..50569a9b6 --- /dev/null +++ b/packages/code-generator/test-cases/rax-app/demo3/expected/demo-project/src/document/index.jsx @@ -0,0 +1,25 @@ +import { createElement } from 'rax'; +import { Root, Style, Script } from 'rax-document'; + +function Document() { + return ( + + + + + Rax App Demo +