diff --git a/packages/code-generator/README.md b/packages/code-generator/README.md index 3534a7e9a..fe71e6c6a 100644 --- a/packages/code-generator/README.md +++ b/packages/code-generator/README.md @@ -1 +1,9 @@ -出码模块 +# 出码模块 + +## 安装接入 + + +## 自定义导出 + + +## 开始开发 diff --git a/packages/code-generator/package.json b/packages/code-generator/package.json new file mode 100644 index 000000000..83ab3df5e --- /dev/null +++ b/packages/code-generator/package.json @@ -0,0 +1,37 @@ +{ + "name": "@ali/lowcode-engine-code-generator", + "version": "0.0.1", + "description": "出码引擎 for LowCode Engine", + "main": "lib/index.js", + "files": [ + "lib" + ], + "scripts": { + "build": "rimraf lib && tsc", + "test": "ava" + }, + "dependencies": { + "@ali/am-eslint-config": "*", + "change-case": "^3.1.0", + "short-uuid": "^3.1.1" + }, + "devDependencies": { + "ava": "^1.0.1", + "rimraf": "^3.0.2", + "ts-node": "^7.0.1" + }, + "ava": { + "compileEnhancements": false, + "snapshotDir": "test/fixtures/__snapshots__", + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" + ] + }, + "publishConfig": { + "registry": "http://registry.npm.alibaba-inc.com" + }, + "license": "MIT" +} diff --git a/packages/code-generator/src/const/generator.ts b/packages/code-generator/src/const/generator.ts new file mode 100644 index 000000000..329adfee5 --- /dev/null +++ b/packages/code-generator/src/const/generator.ts @@ -0,0 +1,13 @@ +export const COMMON_CHUNK_NAME = { + ExternalDepsImport: 'CommonExternalDependencyImport', + InternalDepsImport: 'CommonInternalDependencyImport', + FileVarDefine: 'CommonFileScopeVarDefine', + FileUtilDefine: 'CommonFileScopeMethodDefine', + FileMainContent: 'CommonFileMainContent', + FileExport: 'CommonFileExport', + StyleDepsImport: 'CommonStyleDepsImport', + StyleCssContent: 'CommonStyleCssContent', + HtmlContent: 'CommonHtmlContent', +}; + +export const COMMON_SUB_MODULE_NAME = 'index'; diff --git a/packages/code-generator/src/const/index.ts b/packages/code-generator/src/const/index.ts new file mode 100644 index 000000000..d637ad03c --- /dev/null +++ b/packages/code-generator/src/const/index.ts @@ -0,0 +1,9 @@ +export const NATIVE_ELE_PKG: string = 'native'; + +export const CONTAINER_TYPE = { + COMPONENT: 'Component', + BLOCK: 'Block', + PAGE: 'Page', +}; + +export const SUPPORT_SCHEMA_VERSION_LIST = ['0.0.1']; diff --git a/packages/code-generator/src/demo/loopDemo.ts b/packages/code-generator/src/demo/loopDemo.ts new file mode 100644 index 000000000..cf5b83bc5 --- /dev/null +++ b/packages/code-generator/src/demo/loopDemo.ts @@ -0,0 +1,276 @@ +import { IBasicSchema } from '@/types'; + +const demoData: IBasicSchema = { + version: '1.0.0', + componentsMap: [ + { + componentName: 'Button', + package: '@alifd/next', + version: '1.19.4', + destructuring: true, + exportName: 'Select', + subName: 'Button', + }, + ], + utils: [ + { + name: 'clone', + type: 'npm', + content: { + package: 'lodash', + version: '0.0.1', + exportName: 'clone', + subName: '', + destructuring: false, + main: '/lib/clone', + }, + }, + { + name: 'moment', + type: 'npm', + content: { + package: '@alife/next', + version: '0.0.1', + exportName: 'Moment', + subName: '', + destructuring: true, + main: '', + }, + }, + ], + componentsTree: [ + { + componentName: 'Page', + fileName: 'loopDemo', + props: {}, + children: [ + { + componentName: 'Html', + props: { + html: + '1.选中Col组件,在右侧“数据”面板,设置循环数据;
\n2.给Col组件内的子组件文本内容,绑定对应的数据变量;this.item获取当前循环数据,this.index获取当前循环序号', + }, + }, + { + componentName: 'Row', + props: { + style: { + paddingTop: 30, + paddingRight: 30, + paddingBottom: 30, + paddingLeft: 30, + }, + }, + children: [ + { + componentName: 'Col', + props: {}, + children: [ + { + componentName: 'Text', + props: { + style: { + display: 'block', + marginBottom: 8, + fontWeight: 'bold', + fontSize: 14, + lineHeight: '32px', + }, + text: { + type: 'JSExpression', + value: 'this.item.title', + }, + }, + }, + { + componentName: 'Text', + props: { + style: { + display: 'block', + marginBottom: 12, + fontWeight: 'bold', + fontSize: 16, + color: '#65aa14', + lineHeight: '12px', + }, + text: { + type: 'JSExpression', + value: 'this.item.num', + }, + }, + }, + { + componentName: 'Text', + props: { + style: { + display: 'block', + color: '#9b9b9b', + }, + text: { + type: 'JSExpression', + value: 'this.item.description', + }, + }, + }, + ], + loop: [ + { + title: '活跃UV', + num: 2783, + description: '小二:外包商家:12', + }, + { + title: '活跃PV', + num: 17382, + description: '小二外包商家123', + }, + { + title: '不活跃页面数', + num: 36, + description: '占总页面数比例 30%', + }, + { + title: '人均使用时长', + num: 788, + description: '人均使用频次', + }, + { + title: '新增用户数', + num: 14, + description: '小二:外包:商家 1:1:1', + }, + ], + }, + { + componentName: 'Col', + props: {}, + children: [ + { + componentName: 'Text', + props: { + style: { + display: 'block', + marginBottom: 8, + fontWeight: 'bold', + fontSize: '14px', + lineHeight: '32px', + }, + text: '更多用户数据分析', + }, + }, + { + componentName: 'Button', + props: { + type: 'primary', + style: { + margin: '0 5px 0 5px', + }, + }, + children: '查看详情', + }, + ], + }, + ], + }, + { + componentName: 'Table', + props: { + hasBorder: true, + hasHeader: true, + dataSource: [ + { + id: 1, + name: 'a1', + age: 1, + }, + { + id: 2, + name: 'a2', + age: 2, + }, + { + id: 3, + name: 'a3', + age: 3, + }, + { + id: 4, + name: 'a4', + age: 4, + }, + ], + }, + children: [ + { + componentName: 'TableColumn', + props: { + title: { + type: 'JSExpression', + value: 'this.item.title', + }, + dataIndex: { + type: 'JSExpression', + value: 'this.item.dataIndex', + }, + }, + loop: { + type: 'JSExpression', + value: 'this.state.columns', + }, + }, + ], + }, + ], + state: { + dataSource: [ + { + id: 1, + name: 'a1', + age: 21, + }, + { + id: 2, + name: 'a2', + age: 22, + }, + { + id: 3, + name: 'a3', + age: 23, + }, + { + id: 4, + name: 'a4', + age: 24, + }, + ], + columns: [ + { + title: 'ID', + dataIndex: 'id', + }, + { + title: '姓名', + dataIndex: 'name', + }, + { + title: '年龄', + dataIndex: 'age', + }, + ], + }, + }, + ], + i18n: { + 'zh-CN': { + 'i18n-jwg27yo4': '你好', + 'i18n-jwg27yo3': '中国', + }, + 'en-US': { + 'i18n-jwg27yo4': 'Hello', + 'i18n-jwg27yo3': 'China', + }, + }, +}; + +export default demoData; diff --git a/packages/code-generator/src/demo/main.ts b/packages/code-generator/src/demo/main.ts new file mode 100644 index 000000000..722cff307 --- /dev/null +++ b/packages/code-generator/src/demo/main.ts @@ -0,0 +1,161 @@ +import { IProjectSchema, IResultDir, IResultFile } from '@/types'; + +import CodeGenerator from '@/index'; + +const schema: IProjectSchema = { + version: '1.0.0', + componentsMap: [ + { + componentName: 'Button', + package: 'alife/next', + version: '1.0.0', + destructuring: true, + exportName: 'Select', + subName: 'Button', + }, + ], + componentsTree: [ + { + componentName: 'Page', + fileName: 'Page1', + props: {}, + css: 'body {font-size: 12px;} .table { width: 100px;}', + children: [ + { + componentName: 'Div', + props: { + className: '', + }, + children: [ + { + componentName: 'Button', + props: { + prop1: 1234, + prop2: [ + { + label: '选项1', + value: 1, + }, + { + label: '选项2', + value: 2, + }, + ], + prop3: [ + { + name: 'myName', + rule: { + type: 'JSExpression', + value: '/w+/i', + }, + }, + ], + valueBind: { + type: 'JSExpression', + value: 'this.state.user.name', + }, + onClick: { + type: 'JSExpression', + value: 'function(e) { console.log(e.target.innerText) }', + }, + onClick2: { + type: 'JSExpression', + value: 'this.submit', + }, + }, + }, + ], + }, + ], + }, + ], + utils: [ + { + name: 'clone', + type: 'npm', + content: { + package: 'lodash', + version: '0.0.1', + exportName: 'clone', + subName: '', + destructuring: false, + main: '/lib/clone', + }, + }, + { + name: 'beforeRequestHandler', + type: 'function', + content: { + type: 'JSExpression', + value: 'function(){\n ... \n}', + }, + }, + ], + constants: { + ENV: 'prod', + DOMAIN: 'xxx.alibaba-inc.com', + }, + css: 'body {font-size: 12px;} .table { width: 100px;}', + config: { + sdkVersion: '1.0.3', + historyMode: 'hash', + targetRootID: 'J_Container', + layout: { + componentName: 'BasicLayout', + props: { + logo: '...', + name: '测试网站', + }, + }, + theme: { + package: '@alife/theme-fusion', + version: '^0.1.0', + }, + }, + meta: { + name: 'demo应用', + git_group: 'appGroup', + project_name: 'app_demo', + description: '这是一个测试应用', + spma: 'spa23d', + creator: '月飞', + }, + i18n: { + 'zh-CN': { + i18nJwg27yo4: '你好', + i18nJwg27yo3: '中国', + }, + 'en-US': { + i18nJwg27yo4: 'Hello', + i18nJwg27yo3: 'China', + }, + }, +}; + +function flatFiles(rootName: string | null, dir: IResultDir): IResultFile[] { + const dirRoot: string = rootName ? `${rootName}/${dir.name}` : dir.name; + const files: IResultFile[] = dir.files.map(file => ({ + name: `${dirRoot}/${file.name}.${file.ext}`, + content: file.content, + ext: '', + })); + const filesInSub = dir.dirs.map(subDir => flatFiles(`${dirRoot}`, subDir)); + const result: IResultFile[] = files.concat.apply(files, filesInSub); + + return result; +} + +function main() { + const createIceJsProjectBuilder = CodeGenerator.solutions.icejs; + const builder = createIceJsProjectBuilder(); + builder.generateProject(schema).then(result => { + const files = flatFiles('.', result); + files.forEach(file => { + console.log(`========== ${file.name} Start ==========`); + console.log(file.content); + console.log(`========== ${file.name} End ==========`); + }); + }); +} + +main(); diff --git a/packages/code-generator/src/generator/ChunkBuilder.ts b/packages/code-generator/src/generator/ChunkBuilder.ts new file mode 100644 index 000000000..18ff63991 --- /dev/null +++ b/packages/code-generator/src/generator/ChunkBuilder.ts @@ -0,0 +1,74 @@ +import { + BuilderComponentPlugin, + IChunkBuilder, + ICodeChunk, + ICodeStruct, +} from '../types'; + +import { COMMON_SUB_MODULE_NAME } from '../const/generator'; + +export const groupChunks = (chunks: ICodeChunk[]): ICodeChunk[][] => { + const col = chunks.reduce( + (chunksSet: Record, chunk) => { + const fileKey = `${chunk.subModule || COMMON_SUB_MODULE_NAME}.${ + chunk.fileType + }`; + if (!chunksSet[fileKey]) { + chunksSet[fileKey] = []; + } + chunksSet[fileKey].push(chunk); + return chunksSet; + }, + {}, + ); + + return Object.keys(col).map(key => col[key]); +}; + +/** + * 代码片段构建器 + * + * @export + * @class ChunkBuilder + * @template T + */ +export default class ChunkBuilder implements IChunkBuilder { + private plugins: BuilderComponentPlugin[]; + + constructor(plugins: BuilderComponentPlugin[] = []) { + this.plugins = plugins; + } + + public async run( + ir: unknown, + initialStructure: ICodeStruct = { + ir, + chunks: [], + depNames: [], + }, + ) { + const structure = initialStructure; + + const finalStructure: ICodeStruct = await this.plugins.reduce( + async (previousPluginOperation: Promise, plugin) => { + const modifiedStructure = await previousPluginOperation; + return plugin(modifiedStructure); + }, + Promise.resolve(structure), + ); + + const chunks = groupChunks(finalStructure.chunks); + + return { + chunks, + }; + } + + public getPlugins() { + return this.plugins; + } + + public addPlugin(plugin: BuilderComponentPlugin) { + this.plugins.push(plugin); + } +} diff --git a/packages/code-generator/src/generator/CodeBuilder.ts b/packages/code-generator/src/generator/CodeBuilder.ts new file mode 100644 index 000000000..1e5c3c999 --- /dev/null +++ b/packages/code-generator/src/generator/CodeBuilder.ts @@ -0,0 +1,100 @@ +import { + ChunkContent, + ChunkType, + CodeGeneratorError, + CodeGeneratorFunction, + ICodeBuilder, + ICodeChunk, +} from '../types'; + +export default class Builder implements ICodeBuilder { + private chunkDefinitions: ICodeChunk[] = []; + + private generators: { [key: string]: CodeGeneratorFunction } = { + [ChunkType.STRING]: (str: string) => str, // no-op for string chunks + [ChunkType.JSON]: (json: object) => JSON.stringify(json), // stringify json to string + }; + + constructor(chunkDefinitions: ICodeChunk[] = []) { + this.chunkDefinitions = chunkDefinitions; + } + + /** + * Links all chunks together based on their requirements. Returns an array + * of ordered chunk names which need to be compiled and glued together. + */ + public link(chunkDefinitions: ICodeChunk[] = []): string { + const chunks = chunkDefinitions || this.chunkDefinitions; + if (chunks.length <= 0) { + return ''; + } + + const unprocessedChunks = chunks.map(chunk => { + return { + name: chunk.name, + type: chunk.type, + content: chunk.content, + linkAfter: this.cleanupInvalidChunks(chunk.linkAfter, chunks), + }; + }); + + const resultingString: string[] = []; + + while (unprocessedChunks.length > 0) { + let indexToRemove = 0; + for (let index = 0; index < unprocessedChunks.length; index++) { + if (unprocessedChunks[index].linkAfter.length <= 0) { + indexToRemove = index; + break; + } + } + + if (unprocessedChunks[indexToRemove].linkAfter.length > 0) { + throw new CodeGeneratorError( + 'Operation aborted. Reason: cyclic dependency between chunks.', + ); + } + + const { type, content, name } = unprocessedChunks[indexToRemove]; + const compiledContent = this.generateByType(type, content); + if (compiledContent) { + resultingString.push(compiledContent + '\n'); + } + + unprocessedChunks.splice(indexToRemove, 1); + unprocessedChunks.forEach( + // remove the processed chunk from all the linkAfter arrays from the remaining chunks + ch => (ch.linkAfter = ch.linkAfter.filter(after => after !== name)), + ); + } + + return resultingString.join('\n'); + } + + public generateByType(type: string, content: unknown): string { + if (!content) { + return ''; + } + if (Array.isArray(content)) { + return content + .map(contentItem => this.generateByType(type, contentItem)) + .join('\n'); + } + + if (!this.generators[type]) { + throw new Error( + `Attempted to generate unknown type ${type}. Please register a generator for this type in builder/index.ts`, + ); + } + + return this.generators[type](content); + } + + // remove invalid chunks (which did not end up being created) from the linkAfter fields + // one use-case is when you want to remove the import plugin + private cleanupInvalidChunks(linkAfter: string[], chunks: ICodeChunk[]) { + return linkAfter.filter(chunkName => + chunks.some(chunk => chunk.name === chunkName), + ); + } +} diff --git a/packages/code-generator/src/generator/ModuleBuilder.ts b/packages/code-generator/src/generator/ModuleBuilder.ts new file mode 100644 index 000000000..d52913d35 --- /dev/null +++ b/packages/code-generator/src/generator/ModuleBuilder.ts @@ -0,0 +1,79 @@ +import { + BuilderComponentPlugin, + CodeGeneratorError, + ICodeChunk, + ICompiledModule, + IModuleBuilder, + IResultFile, +} from '../types'; + +import { COMMON_SUB_MODULE_NAME } from '../const/generator'; + +import ChunkBuilder from './ChunkBuilder'; +import CodeBuilder from './CodeBuilder'; + +import ResultFile from '../model/ResultFile'; + +export function createModuleBuilder( + options: { + plugins: BuilderComponentPlugin[]; + mainFileName?: string; + } = { + plugins: [], + }, +): IModuleBuilder { + const chunkGenerator = new ChunkBuilder(options.plugins); + const linker = new CodeBuilder(); + + const generateModule = async (input: unknown): Promise => { + const moduleMainName = options.mainFileName || COMMON_SUB_MODULE_NAME; + if (chunkGenerator.getPlugins().length <= 0) { + throw new CodeGeneratorError( + 'No plugins found. Component generation cannot work without any plugins!', + ); + } + + const files: IResultFile[] = []; + + const { chunks } = await chunkGenerator.run(input); + chunks.forEach(fileChunkList => { + const content = linker.link(fileChunkList); + const file = new ResultFile( + fileChunkList[0].subModule || moduleMainName, + fileChunkList[0].fileType, + content, + ); + files.push(file); + }); + + return { + files, + }; + }; + + const linkCodeChunks = ( + chunks: Record, + fileName: string, + ) => { + const files: IResultFile[] = []; + + Object.keys(chunks).forEach(fileKey => { + const fileChunkList = chunks[fileKey]; + const content = linker.link(fileChunkList); + const file = new ResultFile( + fileChunkList[0].subModule || fileName, + fileChunkList[0].fileType, + content, + ); + files.push(file); + }); + + return files; + }; + + return { + generateModule, + linkCodeChunks, + addPlugin: chunkGenerator.addPlugin.bind(chunkGenerator), + }; +} diff --git a/packages/code-generator/src/generator/ProjectBuilder.ts b/packages/code-generator/src/generator/ProjectBuilder.ts new file mode 100644 index 000000000..cdf727667 --- /dev/null +++ b/packages/code-generator/src/generator/ProjectBuilder.ts @@ -0,0 +1,183 @@ +import { + IModuleBuilder, + IParseResult, + IProjectBuilder, + IProjectPlugins, + IProjectSchema, + IProjectTemplate, + IResultDir, + IResultFile, + ISchemaParser, +} from '../types'; + +import ResultDir from '@/model/ResultDir'; +import SchemaParser from '@/parse/SchemaParser'; + +import { createModuleBuilder } from '@/generator/ModuleBuilder'; + +interface IModuleInfo { + moduleName?: string; + path: string[]; + files: IResultFile[]; +} + +function getDirFromRoot(root: IResultDir, path: string[]): IResultDir { + let current: IResultDir = root; + path.forEach(p => { + const exist = current.dirs.find(d => d.name === p); + if (exist) { + current = exist; + } else { + const newDir = new ResultDir(p); + current.addDirectory(newDir); + current = newDir; + } + }); + + return current; +} + +export class ProjectBuilder implements IProjectBuilder { + private template: IProjectTemplate; + private plugins: IProjectPlugins; + + constructor({ + template, + plugins, + }: { + template: IProjectTemplate; + plugins: IProjectPlugins; + }) { + this.template = template; + this.plugins = plugins; + } + + public async generateProject(schema: IProjectSchema): Promise { + // Init working parts + const schemaParser: ISchemaParser = new SchemaParser(); + const builders = this.createModuleBuilders(); + const projectRoot = this.template.generateTemplate(); + + // Validate + // Parse / Format + + // Preprocess + // Colllect Deps + // Parse JSExpression + const parseResult: IParseResult = schemaParser.parse(schema); + let buildResult: IModuleInfo[] = []; + + // Generator Code module + const containerBuildResult: IModuleInfo[] = await Promise.all( + parseResult.containers.map(async containerInfo => { + let builder: IModuleBuilder; + let path: string[]; + if (containerInfo.containerType === 'Page') { + builder = builders.pages; + path = this.template.slots.pages.path; + } else { + builder = builders.components; + path = this.template.slots.components.path; + } + + const { files } = await builder.generateModule(containerInfo); + + return { + moduleName: containerInfo.fileName, + path, + files, + }; + }), + ); + buildResult = buildResult.concat(containerBuildResult); + + if (parseResult.globalRouter && builders.router) { + const { files } = await builders.router.generateModule( + parseResult.globalRouter, + ); + + buildResult.push({ + path: this.template.slots.pages.path, + files, + }); + } + + // Post Process + + // Combine Modules + buildResult.forEach(moduleInfo => { + let targetDir = getDirFromRoot(projectRoot, moduleInfo.path); + if (moduleInfo.moduleName) { + const dir = new ResultDir(moduleInfo.moduleName); + targetDir.addDirectory(dir); + targetDir = dir; + } + moduleInfo.files.forEach(file => targetDir.addFile(file)); + }); + + return projectRoot; + } + + private createModuleBuilders(): Record { + const builders: Record = {}; + + builders.components = createModuleBuilder({ + plugins: this.plugins.components, + }); + builders.pages = createModuleBuilder({ plugins: this.plugins.pages }); + builders.router = createModuleBuilder({ + plugins: this.plugins.router, + mainFileName: this.template.slots.router.fileName, + }); + builders.entry = createModuleBuilder({ + plugins: this.plugins.entry, + mainFileName: this.template.slots.entry.fileName, + }); + builders.globalStyle = createModuleBuilder({ + plugins: this.plugins.globalStyle, + mainFileName: this.template.slots.globalStyle.fileName, + }); + builders.htmlEntry = createModuleBuilder({ + plugins: this.plugins.htmlEntry, + mainFileName: this.template.slots.htmlEntry.fileName, + }); + builders.packageJSON = createModuleBuilder({ + plugins: this.plugins.packageJSON, + mainFileName: this.template.slots.packageJSON.fileName, + }); + + if (this.template.slots.constants && this.plugins.constants) { + builders.constants = createModuleBuilder({ + plugins: this.plugins.constants, + mainFileName: this.template.slots.constants.fileName, + }); + } + if (this.template.slots.utils && this.plugins.utils) { + builders.utils = createModuleBuilder({ + plugins: this.plugins.utils, + mainFileName: this.template.slots.utils.fileName, + }); + } + if (this.template.slots.i18n && this.plugins.i18n) { + builders.i18n = createModuleBuilder({ + plugins: this.plugins.i18n, + mainFileName: this.template.slots.i18n.fileName, + }); + } + + return builders; + } +} + +export function createProjectBuilder({ + template, + plugins, +}: { + template: IProjectTemplate; + plugins: IProjectPlugins; +}): IProjectBuilder { + return new ProjectBuilder({ + template, + plugins, + }); +} diff --git a/packages/code-generator/src/index.ts b/packages/code-generator/src/index.ts new file mode 100644 index 000000000..12217cc69 --- /dev/null +++ b/packages/code-generator/src/index.ts @@ -0,0 +1,15 @@ +/** + * 低代码引擎的出码模块,负责将编排产出的 Schema 转换成实际可执行的代码。 + * + */ +import { createProjectBuilder } from '@/generator/ProjectBuilder'; +import createIceJsProjectBuilder from '@/solutions/icejs'; + +export * from './types'; + +export default { + createProjectBuilder, + solutions: { + icejs: createIceJsProjectBuilder, + }, +}; diff --git a/packages/code-generator/src/model/ResultDir.ts b/packages/code-generator/src/model/ResultDir.ts new file mode 100644 index 000000000..3888859c1 --- /dev/null +++ b/packages/code-generator/src/model/ResultDir.ts @@ -0,0 +1,31 @@ +import { CodeGeneratorError, IResultDir, IResultFile } from '../types'; + +export default class ResultDir implements IResultDir { + public name: string; + public dirs: IResultDir[]; + public files: IResultFile[]; + + constructor(name: string) { + this.name = name; + this.dirs = []; + this.files = []; + } + + public addDirectory(dir: IResultDir): void { + if (this.dirs.findIndex(d => d.name === dir.name) < 0) { + this.dirs.push(dir); + } else { + throw new CodeGeneratorError('Adding same directory to one directory'); + } + } + + public addFile(file: IResultFile): void { + if ( + this.files.findIndex(f => f.name === file.name && f.ext === file.ext) < 0 + ) { + this.files.push(file); + } else { + throw new CodeGeneratorError('Adding same file to one directory'); + } + } +} diff --git a/packages/code-generator/src/model/ResultFile.ts b/packages/code-generator/src/model/ResultFile.ts new file mode 100644 index 000000000..08b3032b8 --- /dev/null +++ b/packages/code-generator/src/model/ResultFile.ts @@ -0,0 +1,13 @@ +import { IResultFile } from '../types'; + +export default class ResultFile implements IResultFile { + public name: string; + public ext: string; + public content: string; + + constructor(name: string, ext: string = 'jsx', content: string = '') { + this.name = name; + this.ext = ext; + this.content = content; + } +} diff --git a/packages/code-generator/src/parse/SchemaParser.ts b/packages/code-generator/src/parse/SchemaParser.ts new file mode 100644 index 000000000..06c2e0548 --- /dev/null +++ b/packages/code-generator/src/parse/SchemaParser.ts @@ -0,0 +1,181 @@ +/** + * 解析器是对输入的固定格式数据做拆解,使其符合引擎后续步骤预期,完成统一处理逻辑的步骤。 + * 本解析器面向的是标准 schema 协议。 + */ + +import { SUPPORT_SCHEMA_VERSION_LIST } from '../const'; + +import { uniqueArray } from '../utils/common'; + +import { + CodeGeneratorError, + CompatibilityError, + DependencyType, + IBasicSchema, + IComponentNodeItem, + IContainerInfo, + IContainerNodeItem, + IExternalDependency, + IInternalDependency, + InternalDependencyType, + IPageMeta, + IParseResult, + IProjectSchema, + ISchemaParser, + IUtilItem, +} from '../types'; + +const defaultContainer: IContainerInfo = { + containerType: 'Component', + componentName: 'Index', + fileName: 'Index', + css: '', + props: {}, +}; + +class SchemaParser implements ISchemaParser { + public validate(schema: IBasicSchema): boolean { + if (SUPPORT_SCHEMA_VERSION_LIST.indexOf(schema.version) < 0) { + throw new CompatibilityError( + `Not support schema with version [${schema.version}]`, + ); + } + + return true; + } + + public parse(schema: IProjectSchema): IParseResult { + // TODO: collect utils depends in JSExpression + const compDeps: Record = {}; + const internalDeps: Record = {}; + let utilsDeps: IExternalDependency[] = []; + + // 解析三方组件依赖 + schema.componentsMap.forEach(info => { + info.dependencyType = DependencyType.External; + info.importName = info.componentName; + compDeps[info.componentName] = info; + }); + + let containers: IContainerInfo[]; + // Test if this is a lowcode component without container + if (schema.componentsTree.length > 0) { + const firstRoot: IContainerNodeItem = schema + .componentsTree[0] as IContainerNodeItem; + + if (!firstRoot.fileName) { + // 整个 schema 描述一个容器,且无根节点定义 + const container: IContainerInfo = { + ...defaultContainer, + children: schema.componentsTree as IComponentNodeItem[], + }; + containers = [container]; + } else { + // 普通带 1 到多个容器的 schema + containers = schema.componentsTree.map(n => { + const subRoot = n as IContainerNodeItem; + const container: IContainerInfo = { + ...subRoot, + containerType: subRoot.componentName, + componentName: subRoot.fileName, + }; + return container; + }); + } + } else { + throw new CodeGeneratorError(`Can't find anything to generator.`); + } + + // 建立所有容器的内部依赖索引 + containers.forEach(container => { + let type; + switch (container.containerType) { + case 'Page': + type = InternalDependencyType.PAGE; + break; + case 'Block': + type = InternalDependencyType.BLOCK; + break; + default: + type = InternalDependencyType.COMPONENT; + break; + } + + const dep: IInternalDependency = { + type, + moduleName: container.componentName, + destructuring: false, + exportName: container.componentName, + dependencyType: DependencyType.Internal, + }; + + internalDeps[dep.moduleName] = dep; + }); + + // 分析容器内部组件依赖 + containers.forEach(container => { + if (container.children) { + const depNames = this.getComponentNames(container.children); + container.deps = uniqueArray(depNames) + .map(depName => internalDeps[depName] || compDeps[depName]) + .filter(dep => !!dep); + } + }); + + // 分析路由配置 + const routes = containers + .filter(container => container.containerType === 'Page') + .map(page => { + const meta = page.meta as IPageMeta; + return { + path: meta.router, + componentName: page.componentName, + }; + }); + + const routerDeps = routes + .map(r => internalDeps[r.componentName] || compDeps[r.componentName]) + .filter(dep => !!dep); + + // 分析 Utils 依赖 + let utils: IUtilItem[]; + if (schema.utils) { + utils = schema.utils; + utilsDeps = schema.utils + .filter(u => u.type !== 'function') + .map(u => u.content as IExternalDependency); + } else { + utils = []; + } + + return { + containers, + globalUtils: { + utils, + deps: utilsDeps, + }, + globalI18n: schema.i18n, + globalRouter: { + routes, + deps: routerDeps, + }, + project: { + meta: schema.meta, + config: schema.config, + css: schema.css, + constants: schema.constants, + i18n: schema.i18n, + }, + }; + } + + public getComponentNames(list: IComponentNodeItem[]): string[] { + const names = list.map(i => i.componentName); + const namesForward = list + .map(i => this.getComponentNames(i.children || [])) + .reduce((p, c) => p.concat(c), []); + return names.concat(namesForward); + } +} + +export default SchemaParser; diff --git a/packages/code-generator/src/plugins/common/esmodule.ts b/packages/code-generator/src/plugins/common/esmodule.ts new file mode 100644 index 000000000..55853f96c --- /dev/null +++ b/packages/code-generator/src/plugins/common/esmodule.ts @@ -0,0 +1,137 @@ +import { COMMON_CHUNK_NAME } from '../../const/generator'; + +import { + BuilderComponentPlugin, + ChunkType, + CodeGeneratorError, + DependencyType, + FileType, + ICodeChunk, + ICodeStruct, + IDependency, + IExternalDependency, + IInternalDependency, + IWithDependency, +} from '../../types'; + +function groupDepsByPack(deps: IDependency[]): Record { + const depMap: Record = {}; + + const addDep = (pkg: string, dep: IDependency) => { + if (!depMap[pkg]) { + depMap[pkg] = []; + } + depMap[pkg].push(dep); + }; + + deps.forEach(dep => { + if (dep.dependencyType === DependencyType.Internal) { + addDep( + `${(dep as IInternalDependency).moduleName}${dep.main || ''}`, + dep, + ); + } else { + addDep(`${(dep as IExternalDependency).package}${dep.main || ''}`, dep); + } + }); + + return depMap; +} + +function buildPackageImport(pkg: string, deps: IDependency[]): ICodeChunk[] { + const chunks: ICodeChunk[] = []; + let defaultImport: string = ''; + let defaultImportAs: string = ''; + const imports: Record = {}; + + deps.forEach(dep => { + const srcName = dep.exportName; + let targetName = dep.importName || dep.exportName; + + if (dep.subName) { + chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: COMMON_CHUNK_NAME.FileVarDefine, + content: `const ${targetName} = ${srcName}.${dep.subName};`, + linkAfter: [ + COMMON_CHUNK_NAME.ExternalDepsImport, + COMMON_CHUNK_NAME.InternalDepsImport, + ], + }); + + targetName = srcName; + } + + if (dep.destructuring) { + imports[srcName] = targetName; + } else if (defaultImport) { + throw new CodeGeneratorError( + `[${pkg}] has more than one default export.`, + ); + } else { + defaultImport = srcName; + defaultImportAs = targetName; + } + }); + + const items = Object.keys(imports).map(src => + src === imports[src] ? src : `${src} as ${imports[src]}`, + ); + + const statementL = ['import']; + if (defaultImport) { + statementL.push(defaultImportAs); + if (items.length > 0) { + statementL.push(','); + } + } + if (items.length > 0) { + statementL.push(`{ ${items.join(', ')} }`); + } + statementL.push('from'); + + if (deps[0].dependencyType === DependencyType.Internal) { + // TODO: Internal Deps path use project slot setting + statementL.push(`'@src/${(deps[0] as IInternalDependency).type}/${pkg}';`); + chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: COMMON_CHUNK_NAME.InternalDepsImport, + content: statementL.join(' '), + linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport], + }); + } else { + statementL.push(`'${pkg}';`); + chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: COMMON_CHUNK_NAME.ExternalDepsImport, + content: statementL.join(' '), + linkAfter: [], + }); + } + + return chunks; +} + +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IWithDependency; + + if (ir && ir.deps && ir.deps.length > 0) { + const packs = groupDepsByPack(ir.deps); + + Object.keys(packs).forEach(pkg => { + const chunks = buildPackageImport(pkg, packs[pkg]); + next.chunks.push.apply(next.chunks, chunks); + }); + } + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/common/requireUtils.ts b/packages/code-generator/src/plugins/common/requireUtils.ts new file mode 100644 index 000000000..d5b4747b9 --- /dev/null +++ b/packages/code-generator/src/plugins/common/requireUtils.ts @@ -0,0 +1,27 @@ +import { COMMON_CHUNK_NAME } from '../../const/generator'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, +} from '../../types'; + +// TODO: How to merge this logic to common deps +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: COMMON_CHUNK_NAME.InternalDepsImport, + content: `import * from 'react';`, + linkAfter: [], + }); + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/component/react/const.ts b/packages/code-generator/src/plugins/component/react/const.ts new file mode 100644 index 000000000..0aa8b9c30 --- /dev/null +++ b/packages/code-generator/src/plugins/component/react/const.ts @@ -0,0 +1,16 @@ +export const REACT_CHUNK_NAME = { + ClassStart: 'ReactComponentClassDefineStart', + ClassEnd: 'ReactComponentClassDefineEnd', + ClassLifeCycle: 'ReactComponentClassMemberLifeCycle', + ClassMethod: 'ReactComponentClassMemberMethod', + ClassRenderStart: 'ReactComponentClassRenderStart', + ClassRenderPre: 'ReactComponentClassRenderPre', + ClassRenderEnd: 'ReactComponentClassRenderEnd', + ClassRenderJSX: 'ReactComponentClassRenderJSX', + ClassConstructorStart: 'ReactComponentClassConstructorStart', + ClassConstructorEnd: 'ReactComponentClassConstructorEnd', + ClassConstructorContent: 'ReactComponentClassConstructorContent', + ClassDidMountStart: 'ReactComponentClassDidMountStart', + ClassDidMountEnd: 'ReactComponentClassDidMountEnd', + ClassDidMountContent: 'ReactComponentClassDidMountContent', +}; diff --git a/packages/code-generator/src/plugins/component/react/containerClass.ts b/packages/code-generator/src/plugins/component/react/containerClass.ts new file mode 100644 index 000000000..65b11dd57 --- /dev/null +++ b/packages/code-generator/src/plugins/component/react/containerClass.ts @@ -0,0 +1,87 @@ +import { COMMON_CHUNK_NAME } from '../../../const/generator'; +import { REACT_CHUNK_NAME } from './const'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IContainerInfo, +} from '../../../types'; + +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IContainerInfo; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassStart, + content: `class ${ir.componentName} extends React.Component {`, + linkAfter: [ + COMMON_CHUNK_NAME.ExternalDepsImport, + COMMON_CHUNK_NAME.InternalDepsImport, + COMMON_CHUNK_NAME.FileVarDefine, + COMMON_CHUNK_NAME.FileUtilDefine, + ], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassEnd, + content: `}`, + linkAfter: [REACT_CHUNK_NAME.ClassStart, REACT_CHUNK_NAME.ClassRenderEnd], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassConstructorStart, + content: 'constructor(props, context) { super(props); ', + linkAfter: [REACT_CHUNK_NAME.ClassStart], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassConstructorEnd, + content: '}', + linkAfter: [ + REACT_CHUNK_NAME.ClassConstructorStart, + REACT_CHUNK_NAME.ClassConstructorContent, + ], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassRenderStart, + content: 'render() {', + linkAfter: [ + REACT_CHUNK_NAME.ClassStart, + REACT_CHUNK_NAME.ClassConstructorEnd, + REACT_CHUNK_NAME.ClassLifeCycle, + REACT_CHUNK_NAME.ClassMethod, + ], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassRenderEnd, + content: '}', + linkAfter: [ + REACT_CHUNK_NAME.ClassRenderStart, + REACT_CHUNK_NAME.ClassRenderPre, + REACT_CHUNK_NAME.ClassRenderJSX, + ], + }); + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/component/react/containerDataSource.ts b/packages/code-generator/src/plugins/component/react/containerDataSource.ts new file mode 100644 index 000000000..c71859a21 --- /dev/null +++ b/packages/code-generator/src/plugins/component/react/containerDataSource.ts @@ -0,0 +1,39 @@ +import { REACT_CHUNK_NAME } from './const'; + +import { generateCompositeType } from '../../utils/compositeType'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IContainerInfo, +} from '../../../types'; + +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IContainerInfo; + + if (ir.state) { + const state = ir.state; + const fields = Object.keys(state).map(stateName => { + const [isString, value] = generateCompositeType(state[stateName]); + return `${stateName}: ${isString ? `'${value}'` : value},`; + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassConstructorContent, + content: `this.state = { ${fields.join('')} };`, + linkAfter: [REACT_CHUNK_NAME.ClassConstructorStart], + }); + } + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/component/react/containerInitState.ts b/packages/code-generator/src/plugins/component/react/containerInitState.ts new file mode 100644 index 000000000..c71859a21 --- /dev/null +++ b/packages/code-generator/src/plugins/component/react/containerInitState.ts @@ -0,0 +1,39 @@ +import { REACT_CHUNK_NAME } from './const'; + +import { generateCompositeType } from '../../utils/compositeType'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IContainerInfo, +} from '../../../types'; + +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IContainerInfo; + + if (ir.state) { + const state = ir.state; + const fields = Object.keys(state).map(stateName => { + const [isString, value] = generateCompositeType(state[stateName]); + return `${stateName}: ${isString ? `'${value}'` : value},`; + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassConstructorContent, + content: `this.state = { ${fields.join('')} };`, + linkAfter: [REACT_CHUNK_NAME.ClassConstructorStart], + }); + } + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/component/react/containerInjectUtils.ts b/packages/code-generator/src/plugins/component/react/containerInjectUtils.ts new file mode 100644 index 000000000..c2deeaa89 --- /dev/null +++ b/packages/code-generator/src/plugins/component/react/containerInjectUtils.ts @@ -0,0 +1,27 @@ +import { REACT_CHUNK_NAME } from './const'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IContainerInfo, +} from '../../../types'; + +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassConstructorContent, + content: `this.utils = utils;`, + linkAfter: [REACT_CHUNK_NAME.ClassConstructorStart], + }); + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/component/react/containerLifeCycle.ts b/packages/code-generator/src/plugins/component/react/containerLifeCycle.ts new file mode 100644 index 000000000..3e83e659a --- /dev/null +++ b/packages/code-generator/src/plugins/component/react/containerLifeCycle.ts @@ -0,0 +1,81 @@ +import { REACT_CHUNK_NAME } from './const'; + +import { + getFuncExprBody, + transformFuncExpr2MethodMember, +} from '../../utils/jsExpression'; + +import { + BuilderComponentPlugin, + ChunkType, + CodeGeneratorError, + FileType, + ICodeChunk, + ICodeStruct, + IContainerInfo, + IJSExpression, +} from '../../../types'; + +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IContainerInfo; + + if (ir.lifeCycles) { + const lifeCycles = ir.lifeCycles; + const chunks = Object.keys(lifeCycles).map(lifeCycleName => { + if (lifeCycleName === 'constructor') { + return { + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassConstructorContent, + content: getFuncExprBody( + (lifeCycles[lifeCycleName] as IJSExpression).value, + ), + linkAfter: [REACT_CHUNK_NAME.ClassConstructorStart], + }; + } + if (lifeCycleName === 'render') { + return { + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassRenderPre, + content: getFuncExprBody( + (lifeCycles[lifeCycleName] as IJSExpression).value, + ), + linkAfter: [REACT_CHUNK_NAME.ClassRenderStart], + }; + } + if ( + lifeCycleName === 'componentDidMount' || + lifeCycleName === 'componentDidUpdate' || + lifeCycleName === 'componentWillUnmount' || + lifeCycleName === 'componentDidCatch' + ) { + return { + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassLifeCycle, + content: transformFuncExpr2MethodMember( + lifeCycleName, + (lifeCycles[lifeCycleName] as IJSExpression).value, + ), + linkAfter: [ + REACT_CHUNK_NAME.ClassStart, + REACT_CHUNK_NAME.ClassConstructorEnd, + ], + }; + } + + throw new CodeGeneratorError('Unknown life cycle method name'); + }); + + next.chunks.push.apply(next.chunks, chunks); + } + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/component/react/containerMethod.ts b/packages/code-generator/src/plugins/component/react/containerMethod.ts new file mode 100644 index 000000000..cff01d9ce --- /dev/null +++ b/packages/code-generator/src/plugins/component/react/containerMethod.ts @@ -0,0 +1,45 @@ +import { REACT_CHUNK_NAME } from './const'; + +import { transformFuncExpr2MethodMember } from '../../utils/jsExpression'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeChunk, + ICodeStruct, + IContainerInfo, + IJSExpression, +} from '../../../types'; + +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IContainerInfo; + + if (ir.methods) { + const methods = ir.methods; + const chunks = Object.keys(methods).map(methodName => ({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassMethod, + content: transformFuncExpr2MethodMember( + methodName, + (methods[methodName] as IJSExpression).value, + ), + linkAfter: [ + REACT_CHUNK_NAME.ClassStart, + REACT_CHUNK_NAME.ClassConstructorEnd, + REACT_CHUNK_NAME.ClassLifeCycle, + ], + })); + + next.chunks.push.apply(next.chunks, chunks); + } + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/component/react/jsx.ts b/packages/code-generator/src/plugins/component/react/jsx.ts new file mode 100644 index 000000000..ff2a3f8cf --- /dev/null +++ b/packages/code-generator/src/plugins/component/react/jsx.ts @@ -0,0 +1,122 @@ +import { REACT_CHUNK_NAME } from './const'; + +import { generateCompositeType } from '../../utils/compositeType'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IComponentNodeItem, + IContainerInfo, + IInlineStyle, + IJSExpression, +} from '../../../types'; + +function generateInlineStyle(style: IInlineStyle): string | null { + const attrLines = Object.keys(style).map((cssAttribute: string) => { + const [isString, valueStr] = generateCompositeType(style[cssAttribute]); + const valuePart = isString ? `'${valueStr}'` : valueStr; + return `${cssAttribute}: ${valuePart},`; + }); + + if (attrLines.length === 0) { + return null; + } + + return `{ ${attrLines.join('')} }`; +} + +function generateAttr(attrName: string, attrValue: any): string { + const [isString, valueStr] = generateCompositeType(attrValue); + return `${attrName}=${isString ? `"${valueStr}"` : `{${valueStr}}`}`; +} + +function generateNode(nodeItem: IComponentNodeItem): string { + const codePieces: string[] = []; + const { className, style, ...props } = nodeItem.props; + + codePieces.push(`<${nodeItem.componentName}`); + if (className) { + codePieces.push(`className="${className}"`); + } + if (style) { + const inlineStyle = generateInlineStyle(style); + if (inlineStyle !== null) { + codePieces.push(`style={${inlineStyle}}`); + } + } + + const propLines = Object.keys(props).map((propName: string) => + generateAttr(propName, props[propName]), + ); + codePieces.push.apply(codePieces, propLines); + + if (nodeItem.children && nodeItem.children.length > 0) { + codePieces.push('>'); + const childrenLines = nodeItem.children.map(child => generateNode(child)); + codePieces.push.apply(codePieces, childrenLines); + codePieces.push(``); + } else { + codePieces.push('/>'); + } + + if (nodeItem.loop && nodeItem.loopArgs) { + let loopDataExp; + if ((nodeItem.loop as IJSExpression).type === 'JSExpression') { + loopDataExp = `(${(nodeItem.loop as IJSExpression).value})`; + } else { + loopDataExp = JSON.stringify(nodeItem.loop); + } + codePieces.unshift( + `${loopDataExp}.map((${nodeItem.loopArgs[0]}, ${nodeItem.loopArgs[1]}) => (`, + ); + codePieces.push('))'); + } + + if (nodeItem.condition) { + codePieces.unshift(`(${generateCompositeType(nodeItem.condition)}) && (`); + codePieces.push(')'); + } + + if (nodeItem.condition || (nodeItem.loop && nodeItem.loopArgs)) { + codePieces.unshift('{'); + codePieces.push('}'); + } + + return codePieces.join(' '); +} + +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IContainerInfo; + + let jsxContent: string; + if (!ir.children || ir.children.length === 0) { + jsxContent = 'null'; + } else if (ir.children.length === 1) { + jsxContent = `(${generateNode(ir.children[0])})`; + } else { + jsxContent = `(${ir.children + .map(child => generateNode(child)) + .join('')})`; + } + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: REACT_CHUNK_NAME.ClassRenderJSX, + content: `return ${jsxContent};`, + linkAfter: [ + REACT_CHUNK_NAME.ClassRenderStart, + REACT_CHUNK_NAME.ClassRenderPre, + ], + }); + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/component/react/reactCommonDeps.ts b/packages/code-generator/src/plugins/component/react/reactCommonDeps.ts new file mode 100644 index 000000000..38936fc68 --- /dev/null +++ b/packages/code-generator/src/plugins/component/react/reactCommonDeps.ts @@ -0,0 +1,28 @@ +import { COMMON_CHUNK_NAME } from '../../../const/generator'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IContainerInfo, +} from '../../../types'; + +// TODO: How to merge this logic to common deps +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: COMMON_CHUNK_NAME.ExternalDepsImport, + content: `import React from 'react';`, + linkAfter: [], + }); + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/component/style/css.ts b/packages/code-generator/src/plugins/component/style/css.ts new file mode 100644 index 000000000..c4ce2160e --- /dev/null +++ b/packages/code-generator/src/plugins/component/style/css.ts @@ -0,0 +1,37 @@ +import { COMMON_CHUNK_NAME } from '../../../const/generator'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IContainerInfo, +} from '../../../types'; + +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IContainerInfo; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.CSS, + name: COMMON_CHUNK_NAME.StyleCssContent, + content: ir.css, + linkAfter: [COMMON_CHUNK_NAME.StyleDepsImport], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: COMMON_CHUNK_NAME.InternalDepsImport, + content: `import './index.css';`, + linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport], + }); + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/project/constants.ts b/packages/code-generator/src/plugins/project/constants.ts new file mode 100644 index 000000000..6e6e887b6 --- /dev/null +++ b/packages/code-generator/src/plugins/project/constants.ts @@ -0,0 +1,54 @@ +import { COMMON_CHUNK_NAME } from '@/const/generator'; +import { generateCompositeType } from '@/plugins/utils/compositeType'; +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IProjectInfo, +} from '@/types'; + +// TODO: How to merge this logic to common deps +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IProjectInfo; + if (ir.constants) { + const [, constantStr] = generateCompositeType(ir.constants); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JS, + name: COMMON_CHUNK_NAME.FileVarDefine, + content: ` + const constantConfig = ${constantStr}; + `, + linkAfter: [ + COMMON_CHUNK_NAME.ExternalDepsImport, + COMMON_CHUNK_NAME.InternalDepsImport, + ], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JS, + name: COMMON_CHUNK_NAME.FileExport, + content: ` + export default constantConfig; + `, + linkAfter: [ + COMMON_CHUNK_NAME.ExternalDepsImport, + COMMON_CHUNK_NAME.InternalDepsImport, + COMMON_CHUNK_NAME.FileVarDefine, + COMMON_CHUNK_NAME.FileUtilDefine, + COMMON_CHUNK_NAME.FileMainContent, + ], + }); + } + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/project/framework/icejs/plugins/entry.ts b/packages/code-generator/src/plugins/project/framework/icejs/plugins/entry.ts new file mode 100644 index 000000000..488e573b5 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/plugins/entry.ts @@ -0,0 +1,55 @@ +import { COMMON_CHUNK_NAME } from '@/const/generator'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IProjectInfo, +} from '@/types'; + +// TODO: How to merge this logic to common deps +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IProjectInfo; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JS, + name: COMMON_CHUNK_NAME.ExternalDepsImport, + content: ` + import { createApp } from 'ice'; + `, + linkAfter: [], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JS, + name: COMMON_CHUNK_NAME.FileMainContent, + content: ` + const appConfig = { + app: { + rootId: '${ir.config.targetRootID}', + }, + router: { + type: '${ir.config.historyMode}', + }, + }; + createApp(appConfig); + `, + linkAfter: [ + COMMON_CHUNK_NAME.ExternalDepsImport, + COMMON_CHUNK_NAME.InternalDepsImport, + COMMON_CHUNK_NAME.FileVarDefine, + COMMON_CHUNK_NAME.FileUtilDefine, + ], + }); + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/project/framework/icejs/plugins/entryHtml.ts b/packages/code-generator/src/plugins/project/framework/icejs/plugins/entryHtml.ts new file mode 100644 index 000000000..7b12c4252 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/plugins/entryHtml.ts @@ -0,0 +1,43 @@ +import { COMMON_CHUNK_NAME } from '@/const/generator'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IProjectInfo, +} from '@/types'; + +// TODO: How to merge this logic to common deps +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IProjectInfo; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.HTML, + name: COMMON_CHUNK_NAME.HtmlContent, + content: ` + + + + + + + ${ir.meta.name} + + +
+ + + `, + linkAfter: [], + }); + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/project/framework/icejs/plugins/globalStyle.ts b/packages/code-generator/src/plugins/project/framework/icejs/plugins/globalStyle.ts new file mode 100644 index 000000000..9f3bbf142 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/plugins/globalStyle.ts @@ -0,0 +1,53 @@ +import { COMMON_CHUNK_NAME } from '@/const/generator'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IProjectInfo, +} from '@/types'; + +// TODO: How to merge this logic to common deps +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IProjectInfo; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.CSS, + name: COMMON_CHUNK_NAME.StyleDepsImport, + content: ` + // 引入默认全局样式 + @import '@alifd/next/reset.scss'; + `, + linkAfter: [], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.CSS, + name: COMMON_CHUNK_NAME.StyleCssContent, + content: ` + body { + -webkit-font-smoothing: antialiased; + } + `, + linkAfter: [COMMON_CHUNK_NAME.StyleDepsImport], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.CSS, + name: COMMON_CHUNK_NAME.StyleCssContent, + content: ir.css || '', + linkAfter: [COMMON_CHUNK_NAME.StyleDepsImport], + }); + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/project/framework/icejs/plugins/packageJSON.ts b/packages/code-generator/src/plugins/project/framework/icejs/plugins/packageJSON.ts new file mode 100644 index 000000000..b5cac7b4f --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/plugins/packageJSON.ts @@ -0,0 +1,87 @@ +import { COMMON_CHUNK_NAME } from '@/const/generator'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IPackageJSON, + IProjectInfo, +} from '@/types'; + +interface IIceJsPackageJSON extends IPackageJSON { + ideMode: { + name: string; + }; + iceworks: { + type: string; + adapter: string; + }; + originTemplate: string; +} + +// TODO: How to merge this logic to common deps +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IProjectInfo; + + const packageJson: IIceJsPackageJSON = { + name: '@alifd/scaffold-lite-js', + version: '0.1.5', + description: '轻量级模板,使用 JavaScript,仅包含基础的 Layout。', + dependencies: { + '@alifd/next': '^1.19.4', + moment: '^2.24.0', + react: '^16.4.1', + 'react-dom': '^16.4.1', + '@alifd/theme-design-pro': '^0.x', + }, + devDependencies: { + '@ice/spec': '^1.0.0', + 'build-plugin-fusion': '^0.1.0', + 'build-plugin-moment-locales': '^0.1.0', + eslint: '^6.0.1', + 'ice.js': '^1.0.0', + stylelint: '^13.2.0', + '@ali/build-plugin-ice-def': '^0.1.0', + }, + scripts: { + start: 'icejs start', + build: 'icejs build', + lint: 'npm run eslint && npm run stylelint', + eslint: 'eslint --cache --ext .js,.jsx ./', + stylelint: 'stylelint ./**/*.scss', + }, + ideMode: { + name: 'ice-react', + }, + iceworks: { + type: 'react', + adapter: 'adapter-react-v3', + }, + engines: { + node: '>=8.0.0', + }, + repository: { + type: 'git', + url: 'http://gitlab.alibaba-inc.com/msd/leak-scan/tree/master', + }, + private: true, + originTemplate: '@alifd/scaffold-lite-js', + }; + + next.chunks.push({ + type: ChunkType.JSON, + fileType: FileType.JSON, + name: COMMON_CHUNK_NAME.FileMainContent, + content: packageJson, + linkAfter: [], + }); + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/project/framework/icejs/plugins/router.ts b/packages/code-generator/src/plugins/project/framework/icejs/plugins/router.ts new file mode 100644 index 000000000..29a9dd995 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/plugins/router.ts @@ -0,0 +1,79 @@ +import { COMMON_CHUNK_NAME } from '@/const/generator'; + +import { + BuilderComponentPlugin, + ChunkType, + FileType, + ICodeStruct, + IRouterInfo, +} from '@/types'; + +// TODO: How to merge this logic to common deps +const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IRouterInfo; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JS, + name: COMMON_CHUNK_NAME.InternalDepsImport, + content: ` + import BasicLayout from '@/layouts/BasicLayout'; + `, + linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JS, + name: COMMON_CHUNK_NAME.FileVarDefine, + content: ` + const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + ${ir.routes + .map( + route => ` + { + path: '${route.path}', + component: ${route.componentName}, + } + `, + ) + .join(',')} + ], + }, + ]; + `, + linkAfter: [ + COMMON_CHUNK_NAME.ExternalDepsImport, + COMMON_CHUNK_NAME.InternalDepsImport, + COMMON_CHUNK_NAME.FileUtilDefine, + ], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JS, + name: COMMON_CHUNK_NAME.FileExport, + content: ` + export default routerConfig; + `, + linkAfter: [ + COMMON_CHUNK_NAME.ExternalDepsImport, + COMMON_CHUNK_NAME.InternalDepsImport, + COMMON_CHUNK_NAME.FileUtilDefine, + COMMON_CHUNK_NAME.FileVarDefine, + COMMON_CHUNK_NAME.FileMainContent, + ], + }); + + return next; +}; + +export default plugin; diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/README.md.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/README.md.ts new file mode 100644 index 000000000..62933484a --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/README.md.ts @@ -0,0 +1,73 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + 'README', + 'md', + ` +## Scaffold Lite + +> 轻量级模板,使用 JavaScript,仅包含基础的 Layout。 + +## 使用 + +\`\`\`bash +# 安装依赖 +$ npm install + +# 启动服务 +$ npm start # visit http://localhost:3333 +\`\`\` + +[More docs](https://ice.work/docs/guide/about). + +## 目录 + +\`\`\`md +├── build/ # 构建产物 +├── mock/ # 本地模拟数据 +│ ├── index.[j,t]s +├── public/ +│ ├── index.html # 应用入口 HTML +│ └── favicon.png # Favicon +├── src/ # 源码路径 +│ ├── components/ # 自定义业务组件 +│ │ └── Guide/ +│ │ ├── index.[j,t]sx +│ │ ├── index.module.scss +│ ├── layouts/ # 布局组件 +│ │ └── BasicLayout/ +│ │ ├── index.[j,t]sx +│ │ └── index.module.scss +│ ├── pages/ # 页面 +│ │ └── Home/ # home 页面,约定路由转成小写 +│ │ ├── components/ # 页面级自定义业务组件 +│ │ ├── models.[j,t]sx # 页面级数据状态 +│ │ ├── index.[j,t]sx # 页面入口 +│ │ └── index.module.scss # 页面样式文件 +│ ├── configs/ # [可选] 配置文件 +│ │ └── menu.[j,t]s # [可选] 菜单配置 +│ ├── models/ # [可选] 应用级数据状态 +│ │ └── user.[j,t]s +│ ├── utils/ # [可选] 工具库 +│ ├── global.scss # 全局样式 +│ ├── routes.[j,t]s # 路由配置 +│ └── app.[j,t]s[x] # 应用入口脚本 +├── build.json # 工程配置 +├── README.md +├── package.json +├── .editorconfig +├── .eslintignore +├── .eslintrc.[j,t]s +├── .gitignore +├── .stylelintignore +├── .stylelintrc.[j,t]s +├── .gitignore +└── [j,t]sconfig.json +\`\`\` + `, + ); + + return [[], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/abc.json.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/abc.json.ts new file mode 100644 index 000000000..b7e2a9565 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/abc.json.ts @@ -0,0 +1,17 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + 'abc', + 'json', + ` +{ + "type": "ice-app", + "builder": "@ali/builder-ice-app" +} + `, + ); + + return [[], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/build.json.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/build.json.ts new file mode 100644 index 000000000..359a1b6b5 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/build.json.ts @@ -0,0 +1,33 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + 'build', + 'json', + ` +{ + "entry": "src/app.js", + "plugins": [ + [ + "build-plugin-fusion", + { + "themePackage": "@alifd/theme-design-pro" + } + ], + [ + "build-plugin-moment-locales", + { + "locales": [ + "zh-cn" + ] + } + ], + "@ali/build-plugin-ice-def" + ] +} + `, + ); + + return [[], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/editorconfig.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/editorconfig.ts new file mode 100644 index 000000000..445fe9647 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/editorconfig.ts @@ -0,0 +1,25 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + '.editorconfig', + '', + ` +# 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 + `, + ); + + return [[], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/eslintignore.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/eslintignore.ts new file mode 100644 index 000000000..f115ec386 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/eslintignore.ts @@ -0,0 +1,28 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + '.eslintignore', + '', + ` +# 忽略目录 +build/ +tests/ +demo/ +.ice/ + +# node 覆盖率文件 +coverage/ + +# 忽略文件 +**/*-min.js +**/*.min.js + +package-lock.json +yarn.lock + `, + ); + + return [[], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/eslintrc.js.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/eslintrc.js.ts new file mode 100644 index 000000000..02aed6eec --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/eslintrc.js.ts @@ -0,0 +1,16 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + '.eslintrc', + 'js', + ` +const { eslint } = require('@ice/spec'); + +module.exports = eslint; + `, + ); + + return [[], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/gitignore.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/gitignore.ts new file mode 100644 index 000000000..e4c31b7ca --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/gitignore.ts @@ -0,0 +1,36 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + '.gitignore', + '', + ` +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + `, + ); + + return [[], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/jsconfig.json.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/jsconfig.json.ts new file mode 100644 index 000000000..387a02b1b --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/jsconfig.json.ts @@ -0,0 +1,24 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + 'jsconfig', + 'json', + ` +{ + "compilerOptions": { + "baseUrl": ".", + "jsx": "react", + "paths": { + "@/*": ["./src/*"], + "ice": [".ice/index.ts"], + "ice/*": [".ice/pages/*"] + } + } +} + `, + ); + + return [[], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/prettierignore.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/prettierignore.ts new file mode 100644 index 000000000..f50c843d1 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/prettierignore.ts @@ -0,0 +1,22 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + '.prettierignore', + '', + ` +build/ +tests/ +demo/ +.ice/ +coverage/ +**/*-min.js +**/*.min.js +package-lock.json +yarn.lock + `, + ); + + return [[], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/prettierrc.js.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/prettierrc.js.ts new file mode 100644 index 000000000..83b4dc8b7 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/prettierrc.js.ts @@ -0,0 +1,16 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + '.prettierrc', + 'js', + ` +const { prettier } = require('@ice/spec'); + +module.exports = prettier; + `, + ); + + return [[], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Footer/index.jsx.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Footer/index.jsx.ts new file mode 100644 index 000000000..aa1f1bd08 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Footer/index.jsx.ts @@ -0,0 +1,25 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + 'index', + 'jsx', + ` +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( +

+ Alibaba Fusion +
+ © 2019-现在 Alibaba Fusion & ICE +

+ ); +} + `, + ); + + return [['src', 'layouts', 'BasicLayout', 'components', 'Footer'], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Footer/index.module.scss.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Footer/index.module.scss.ts new file mode 100644 index 000000000..3f1f98511 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Footer/index.module.scss.ts @@ -0,0 +1,26 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + 'index', + 'module.scss', + ` +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + `, + ); + + return [['src', 'layouts', 'BasicLayout', 'components', 'Footer'], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Logo/index.jsx.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Logo/index.jsx.ts new file mode 100644 index 000000000..517972a5b --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Logo/index.jsx.ts @@ -0,0 +1,27 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + 'index', + 'jsx', + ` +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( +
+ + {image && logo} + {text} + +
+ ); +} + `, + ); + + return [['src', 'layouts', 'BasicLayout', 'components', 'Logo'], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Logo/index.module.scss.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Logo/index.module.scss.ts new file mode 100644 index 000000000..214dcf88a --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/Logo/index.module.scss.ts @@ -0,0 +1,31 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + 'index', + 'module.scss', + ` +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: $color-text1-1; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: $color-text1-1; + } + + img { + height: 24px; + margin-right: 10px; + } +} + `, + ); + + return [['src', 'layouts', 'BasicLayout', 'components', 'Logo'], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/PageNav/index.jsx.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/PageNav/index.jsx.ts new file mode 100644 index 000000000..4e81e4b67 --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/components/PageNav/index.jsx.ts @@ -0,0 +1,81 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + 'index', + 'jsx', + ` +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, withRouter } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + + {childrenItems} + + ); + return subNav; + } + + return null; + } + + const navItem = ( + + {item.name} + + ); + return navItem; +} + +const Navigation = (props, context) => { + const { location } = props; + const { pathname } = location; + const { isCollapse } = context; + return ( + + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +const PageNav = withRouter(Navigation); +export default PageNav; + `, + ); + + return [['src', 'layouts', 'BasicLayout', 'components', 'PageNav'], file]; +} diff --git a/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/index.jsx.ts b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/index.jsx.ts new file mode 100644 index 000000000..6c3fd945e --- /dev/null +++ b/packages/code-generator/src/plugins/project/framework/icejs/template/files/src/layouts/BasicLayout/index.jsx.ts @@ -0,0 +1,92 @@ +import ResultFile from '@/model/ResultFile'; +import { IResultFile } from '@/types'; + +export default function getFile(): [string[], IResultFile] { + const file = new ResultFile( + 'index', + 'jsx', + ` +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + + + + + + + + + + + + {children} + +