mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-13 04:03:07 +00:00
Merge branch 'feat/code-generator' into release/331
This commit is contained in:
commit
93a4e82a91
@ -1 +1,9 @@
|
||||
出码模块
|
||||
# 出码模块
|
||||
|
||||
## 安装接入
|
||||
|
||||
|
||||
## 自定义导出
|
||||
|
||||
|
||||
## 开始开发
|
||||
|
||||
39
packages/code-generator/package.json
Normal file
39
packages/code-generator/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"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",
|
||||
"demo": "ts-node -r tsconfig-paths/register ./src/demo/main.ts",
|
||||
"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",
|
||||
"tsconfig-paths": "^3.9.0"
|
||||
},
|
||||
"ava": {
|
||||
"compileEnhancements": false,
|
||||
"snapshotDir": "test/fixtures/__snapshots__",
|
||||
"extensions": [
|
||||
"ts"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "http://registry.npm.alibaba-inc.com"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
13
packages/code-generator/src/const/generator.ts
Normal file
13
packages/code-generator/src/const/generator.ts
Normal file
@ -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';
|
||||
9
packages/code-generator/src/const/index.ts
Normal file
9
packages/code-generator/src/const/index.ts
Normal file
@ -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'];
|
||||
53
packages/code-generator/src/demo/main.ts
Normal file
53
packages/code-generator/src/demo/main.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { IResultDir, IResultFile } from '@/types';
|
||||
|
||||
import CodeGenerator from '@/index';
|
||||
import { createDiskPublisher } from '@/publisher/disk';
|
||||
import demoSchema from './simpleDemo';
|
||||
|
||||
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 displayResultInConsole(root: IResultDir, fileName?: string): void {
|
||||
const files = flatFiles('.', root);
|
||||
files.forEach(file => {
|
||||
if (!fileName || fileName === file.name) {
|
||||
console.log(`========== ${file.name} Start ==========`);
|
||||
console.log(file.content);
|
||||
console.log(`========== ${file.name} End ==========`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function writeResultToDisk(root: IResultDir, path: string): Promise<any> {
|
||||
const publisher = createDiskPublisher();
|
||||
|
||||
return publisher.publish({
|
||||
project: root,
|
||||
outputPath: path,
|
||||
projectSlug: 'demo-project',
|
||||
createProjectFolder: true,
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
const createIceJsProjectBuilder = CodeGenerator.solutions.icejs;
|
||||
const builder = createIceJsProjectBuilder();
|
||||
builder.generateProject(demoSchema).then(result => {
|
||||
// displayResultInConsole(result, '././src/routes.js');
|
||||
writeResultToDisk(result, '/Users/armslave/lowcodeDemo').then(response =>
|
||||
console.log('Write to disk: ', JSON.stringify(response)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
239
packages/code-generator/src/demo/simpleDemo.ts
Normal file
239
packages/code-generator/src/demo/simpleDemo.ts
Normal file
@ -0,0 +1,239 @@
|
||||
import { IProjectSchema } from '@/types';
|
||||
|
||||
// meta: {
|
||||
// title: '测试',
|
||||
// router: '/',
|
||||
// },
|
||||
|
||||
const demoData: IProjectSchema = {
|
||||
version: '1.0.0',
|
||||
componentsMap: [
|
||||
{
|
||||
componentName: 'Button',
|
||||
package: '@alifd/next',
|
||||
version: '1.19.18',
|
||||
destructuring: true,
|
||||
exportName: 'Button',
|
||||
},
|
||||
{
|
||||
componentName: 'Button.Group',
|
||||
package: '@alifd/next',
|
||||
version: '1.19.18',
|
||||
destructuring: true,
|
||||
exportName: 'Button',
|
||||
subName: 'Group',
|
||||
},
|
||||
{
|
||||
componentName: 'Input',
|
||||
package: '@alifd/next',
|
||||
version: '1.19.18',
|
||||
destructuring: true,
|
||||
exportName: 'Input',
|
||||
},
|
||||
{
|
||||
componentName: 'Form',
|
||||
package: '@alifd/next',
|
||||
version: '1.19.18',
|
||||
destructuring: true,
|
||||
exportName: 'Form',
|
||||
},
|
||||
{
|
||||
componentName: 'Form.Item',
|
||||
package: '@alifd/next',
|
||||
version: '1.19.18',
|
||||
destructuring: true,
|
||||
exportName: 'Form',
|
||||
subName: 'Item',
|
||||
},
|
||||
{
|
||||
componentName: 'NumberPicker',
|
||||
package: '@alifd/next',
|
||||
version: '1.19.18',
|
||||
destructuring: true,
|
||||
exportName: 'NumberPicker',
|
||||
},
|
||||
{
|
||||
componentName: 'Select',
|
||||
package: '@alifd/next',
|
||||
version: '1.19.18',
|
||||
destructuring: true,
|
||||
exportName: 'Select',
|
||||
},
|
||||
],
|
||||
componentsTree: [
|
||||
{
|
||||
componentName: 'Page',
|
||||
id: 'node$1',
|
||||
meta: {
|
||||
title: '测试',
|
||||
router: '/',
|
||||
},
|
||||
props: {
|
||||
ref: 'outterView',
|
||||
autoLoading: true,
|
||||
},
|
||||
fileName: 'test',
|
||||
state: {
|
||||
text: 'outter',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Form',
|
||||
id: 'node$2',
|
||||
props: {
|
||||
labelCol: 4,
|
||||
style: {},
|
||||
ref: 'testForm',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Form.Item',
|
||||
id: 'node$3',
|
||||
props: {
|
||||
label: '姓名:',
|
||||
name: 'name',
|
||||
initValue: '李雷',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Input',
|
||||
id: 'node$4',
|
||||
props: {
|
||||
placeholder: '请输入',
|
||||
size: 'medium',
|
||||
style: {
|
||||
width: 320,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'Form.Item',
|
||||
id: 'node$5',
|
||||
props: {
|
||||
label: '年龄:',
|
||||
name: 'age',
|
||||
initValue: '22',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'NumberPicker',
|
||||
id: 'node$6',
|
||||
props: {
|
||||
size: 'medium',
|
||||
type: 'normal',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'Form.Item',
|
||||
id: 'node$7',
|
||||
props: {
|
||||
label: '职业:',
|
||||
name: 'profession',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Select',
|
||||
id: 'node$8',
|
||||
props: {
|
||||
dataSource: [
|
||||
{
|
||||
label: '教师',
|
||||
value: 't',
|
||||
},
|
||||
{
|
||||
label: '医生',
|
||||
value: 'd',
|
||||
},
|
||||
{
|
||||
label: '歌手',
|
||||
value: 's',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'Div',
|
||||
id: 'node$9',
|
||||
props: {
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Button.Group',
|
||||
id: 'node$a',
|
||||
props: {},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Button',
|
||||
id: 'node$b',
|
||||
props: {
|
||||
type: 'primary',
|
||||
style: {
|
||||
margin: '0 5px 0 5px',
|
||||
},
|
||||
htmlType: 'submit',
|
||||
},
|
||||
children: ['提交'],
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
id: 'node$d',
|
||||
props: {
|
||||
type: 'normal',
|
||||
style: {
|
||||
margin: '0 5px 0 5px',
|
||||
},
|
||||
htmlType: 'reset',
|
||||
},
|
||||
children: ['重置'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
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',
|
||||
primary: '#ff9966',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
name: 'demo应用',
|
||||
git_group: 'appGroup',
|
||||
project_name: 'app_demo',
|
||||
description: '这是一个测试应用',
|
||||
spma: 'spa23d',
|
||||
creator: '月飞',
|
||||
},
|
||||
};
|
||||
|
||||
export default demoData;
|
||||
74
packages/code-generator/src/generator/ChunkBuilder.ts
Normal file
74
packages/code-generator/src/generator/ChunkBuilder.ts
Normal file
@ -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<string, ICodeChunk[]>, 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<ICodeStruct>, 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);
|
||||
}
|
||||
}
|
||||
100
packages/code-generator/src/generator/CodeBuilder.ts
Normal file
100
packages/code-generator/src/generator/CodeBuilder.ts
Normal file
@ -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<ChunkContent> } = {
|
||||
[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),
|
||||
);
|
||||
}
|
||||
}
|
||||
79
packages/code-generator/src/generator/ModuleBuilder.ts
Normal file
79
packages/code-generator/src/generator/ModuleBuilder.ts
Normal file
@ -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<ICompiledModule> => {
|
||||
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<string, ICodeChunk[]>,
|
||||
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),
|
||||
};
|
||||
}
|
||||
272
packages/code-generator/src/generator/ProjectBuilder.ts
Normal file
272
packages/code-generator/src/generator/ProjectBuilder.ts
Normal file
@ -0,0 +1,272 @@
|
||||
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<IResultDir> {
|
||||
// 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
|
||||
// components
|
||||
// pages
|
||||
const containerBuildResult: IModuleInfo[] = await Promise.all<IModuleInfo>(
|
||||
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);
|
||||
|
||||
// router
|
||||
if (parseResult.globalRouter && builders.router) {
|
||||
const { files } = await builders.router.generateModule(
|
||||
parseResult.globalRouter,
|
||||
);
|
||||
|
||||
buildResult.push({
|
||||
path: this.template.slots.router.path,
|
||||
files,
|
||||
});
|
||||
}
|
||||
|
||||
// entry
|
||||
if (parseResult.project && builders.entry) {
|
||||
const { files } = await builders.entry.generateModule(
|
||||
parseResult.project,
|
||||
);
|
||||
|
||||
buildResult.push({
|
||||
path: this.template.slots.entry.path,
|
||||
files,
|
||||
});
|
||||
}
|
||||
// constants?
|
||||
if (
|
||||
parseResult.project &&
|
||||
builders.constants &&
|
||||
this.template.slots.constants
|
||||
) {
|
||||
const { files } = await builders.constants.generateModule(
|
||||
parseResult.project,
|
||||
);
|
||||
|
||||
buildResult.push({
|
||||
path: this.template.slots.constants.path,
|
||||
files,
|
||||
});
|
||||
}
|
||||
// utils?
|
||||
if (
|
||||
parseResult.globalUtils &&
|
||||
builders.utils &&
|
||||
this.template.slots.utils
|
||||
) {
|
||||
const { files } = await builders.utils.generateModule(
|
||||
parseResult.globalUtils,
|
||||
);
|
||||
|
||||
buildResult.push({
|
||||
path: this.template.slots.utils.path,
|
||||
files,
|
||||
});
|
||||
}
|
||||
// i18n?
|
||||
if (parseResult.globalI18n && builders.i18n && this.template.slots.i18n) {
|
||||
const { files } = await builders.i18n.generateModule(
|
||||
parseResult.globalI18n,
|
||||
);
|
||||
|
||||
buildResult.push({
|
||||
path: this.template.slots.i18n.path,
|
||||
files,
|
||||
});
|
||||
}
|
||||
// globalStyle
|
||||
if (parseResult.project && builders.globalStyle) {
|
||||
const { files } = await builders.globalStyle.generateModule(
|
||||
parseResult.project,
|
||||
);
|
||||
|
||||
buildResult.push({
|
||||
path: this.template.slots.globalStyle.path,
|
||||
files,
|
||||
});
|
||||
}
|
||||
// htmlEntry
|
||||
if (parseResult.project && builders.htmlEntry) {
|
||||
const { files } = await builders.htmlEntry.generateModule(
|
||||
parseResult.project,
|
||||
);
|
||||
|
||||
buildResult.push({
|
||||
path: this.template.slots.htmlEntry.path,
|
||||
files,
|
||||
});
|
||||
}
|
||||
// packageJSON
|
||||
if (parseResult.project && builders.packageJSON) {
|
||||
const { files } = await builders.packageJSON.generateModule(
|
||||
parseResult.project,
|
||||
);
|
||||
|
||||
buildResult.push({
|
||||
path: this.template.slots.packageJSON.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<string, IModuleBuilder> {
|
||||
const builders: Record<string, IModuleBuilder> = {};
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
15
packages/code-generator/src/index.ts
Normal file
15
packages/code-generator/src/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 低代码引擎的出码模块,负责将编排产出的 Schema 转换成实际可执行的代码。
|
||||
*
|
||||
*/
|
||||
import { createProjectBuilder } from '@/generator/ProjectBuilder';
|
||||
import createIceJsProjectBuilder from '@/solutions/icejs';
|
||||
|
||||
export * from './types';
|
||||
|
||||
export default {
|
||||
createProjectBuilder,
|
||||
solutions: {
|
||||
icejs: createIceJsProjectBuilder,
|
||||
},
|
||||
};
|
||||
31
packages/code-generator/src/model/ResultDir.ts
Normal file
31
packages/code-generator/src/model/ResultDir.ts
Normal file
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
13
packages/code-generator/src/model/ResultFile.ts
Normal file
13
packages/code-generator/src/model/ResultFile.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
185
packages/code-generator/src/parse/SchemaParser.ts
Normal file
185
packages/code-generator/src/parse/SchemaParser.ts
Normal file
@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 解析器是对输入的固定格式数据做拆解,使其符合引擎后续步骤预期,完成统一处理逻辑的步骤。
|
||||
* 本解析器面向的是标准 schema 协议。
|
||||
*/
|
||||
|
||||
import { SUPPORT_SCHEMA_VERSION_LIST } from '../const';
|
||||
|
||||
import { handleChildren } from '../utils/children';
|
||||
import { uniqueArray } from '../utils/common';
|
||||
|
||||
import {
|
||||
ChildNodeItem,
|
||||
ChildNodeType,
|
||||
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<string, IExternalDependency> = {};
|
||||
const internalDeps: Record<string, IInternalDependency> = {};
|
||||
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<string>(depNames)
|
||||
// .map(depName => internalDeps[depName] || compDeps[depName])
|
||||
// .filter(dep => !!dep);
|
||||
container.deps = Object.keys(compDeps).map(
|
||||
depName => compDeps[depName],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 分析路由配置
|
||||
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(children: ChildNodeType): string[] {
|
||||
return handleChildren<string>(children, {
|
||||
node: (i: IComponentNodeItem) => [i.componentName],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default SchemaParser;
|
||||
146
packages/code-generator/src/plugins/common/esmodule.ts
Normal file
146
packages/code-generator/src/plugins/common/esmodule.ts
Normal file
@ -0,0 +1,146 @@
|
||||
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<string, IDependency[]> {
|
||||
const depMap: Record<string, IDependency[]> = {};
|
||||
|
||||
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[],
|
||||
isJSX: boolean,
|
||||
): ICodeChunk[] {
|
||||
const chunks: ICodeChunk[] = [];
|
||||
let defaultImport: string = '';
|
||||
let defaultImportAs: string = '';
|
||||
const imports: Record<string, string> = {};
|
||||
|
||||
deps.forEach(dep => {
|
||||
const srcName = dep.exportName;
|
||||
let targetName = dep.importName || dep.exportName;
|
||||
if (dep.subName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dep.subName) {
|
||||
chunks.push({
|
||||
type: ChunkType.STRING,
|
||||
fileType: isJSX ? FileType.JSX : FileType.JS,
|
||||
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(`'@/${(deps[0] as IInternalDependency).type}/${pkg}';`);
|
||||
chunks.push({
|
||||
type: ChunkType.STRING,
|
||||
fileType: isJSX ? FileType.JSX : FileType.JS,
|
||||
name: COMMON_CHUNK_NAME.InternalDepsImport,
|
||||
content: statementL.join(' '),
|
||||
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport],
|
||||
});
|
||||
} else {
|
||||
statementL.push(`'${pkg}';`);
|
||||
chunks.push({
|
||||
type: ChunkType.STRING,
|
||||
fileType: isJSX ? FileType.JSX : FileType.JS,
|
||||
name: COMMON_CHUNK_NAME.ExternalDepsImport,
|
||||
content: statementL.join(' '),
|
||||
linkAfter: [],
|
||||
});
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
|
||||
const next: ICodeStruct = {
|
||||
...pre,
|
||||
};
|
||||
|
||||
const isJSX = next.chunks.some(chunk => chunk.fileType === FileType.JSX);
|
||||
|
||||
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], isJSX);
|
||||
next.chunks.push.apply(next.chunks, chunks);
|
||||
});
|
||||
}
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
27
packages/code-generator/src/plugins/common/requireUtils.ts
Normal file
27
packages/code-generator/src/plugins/common/requireUtils.ts
Normal file
@ -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;
|
||||
16
packages/code-generator/src/plugins/component/react/const.ts
Normal file
16
packages/code-generator/src/plugins/component/react/const.ts
Normal file
@ -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',
|
||||
};
|
||||
@ -0,0 +1,101 @@
|
||||
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,
|
||||
],
|
||||
});
|
||||
|
||||
next.chunks.push({
|
||||
type: ChunkType.STRING,
|
||||
fileType: FileType.JSX,
|
||||
name: COMMON_CHUNK_NAME.FileExport,
|
||||
content: `export default ${ir.componentName};`,
|
||||
linkAfter: [
|
||||
COMMON_CHUNK_NAME.ExternalDepsImport,
|
||||
COMMON_CHUNK_NAME.InternalDepsImport,
|
||||
COMMON_CHUNK_NAME.FileVarDefine,
|
||||
COMMON_CHUNK_NAME.FileUtilDefine,
|
||||
REACT_CHUNK_NAME.ClassEnd,
|
||||
],
|
||||
});
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@ -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<string>(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;
|
||||
@ -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<string>(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;
|
||||
@ -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;
|
||||
@ -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<ICodeChunk>(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;
|
||||
@ -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<ICodeChunk>(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;
|
||||
149
packages/code-generator/src/plugins/component/react/jsx.ts
Normal file
149
packages/code-generator/src/plugins/component/react/jsx.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import {
|
||||
BuilderComponentPlugin,
|
||||
ChildNodeItem,
|
||||
ChildNodeType,
|
||||
ChunkType,
|
||||
FileType,
|
||||
ICodeStruct,
|
||||
IComponentNodeItem,
|
||||
IContainerInfo,
|
||||
IInlineStyle,
|
||||
IJSExpression,
|
||||
} from '../../../types';
|
||||
|
||||
import { handleChildren } from '@/utils/children';
|
||||
import { generateCompositeType } from '../../utils/compositeType';
|
||||
import { REACT_CHUNK_NAME } from './const';
|
||||
|
||||
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 {
|
||||
if (attrName === 'initValue' || attrName === 'labelCol') {
|
||||
return '';
|
||||
}
|
||||
const [isString, valueStr] = generateCompositeType(attrValue);
|
||||
return `${attrName}=${isString ? `"${valueStr}"` : `{${valueStr}}`}`;
|
||||
}
|
||||
|
||||
function mapNodeName(src: string): string {
|
||||
if (src === 'Div') {
|
||||
return 'div';
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
function generateNode(nodeItem: IComponentNodeItem): string {
|
||||
const codePieces: string[] = [];
|
||||
let propLines: string[] = [];
|
||||
const { className, style, ...props } = nodeItem.props;
|
||||
|
||||
codePieces.push(`<${mapNodeName(nodeItem.componentName)}`);
|
||||
if (className) {
|
||||
propLines.push(`className="${className}"`);
|
||||
}
|
||||
if (style) {
|
||||
const inlineStyle = generateInlineStyle(style);
|
||||
if (inlineStyle !== null) {
|
||||
propLines.push(`style={${inlineStyle}}`);
|
||||
}
|
||||
}
|
||||
|
||||
propLines = propLines.concat(
|
||||
Object.keys(props).map((propName: string) =>
|
||||
generateAttr(propName, props[propName]),
|
||||
),
|
||||
);
|
||||
codePieces.push(` ${propLines.join(' ')} `);
|
||||
|
||||
if (nodeItem.children && (nodeItem.children as unknown[]).length > 0) {
|
||||
codePieces.push('>');
|
||||
const childrenLines = generateChildren(nodeItem.children);
|
||||
codePieces.push.apply(codePieces, childrenLines);
|
||||
codePieces.push(`</${mapNodeName(nodeItem.componentName)}>`);
|
||||
} 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('');
|
||||
}
|
||||
|
||||
function generateChildren(children: ChildNodeType): string[] {
|
||||
return handleChildren<string>(children, {
|
||||
// TODO: 如果容器直接只有一个 字符串 children 呢?
|
||||
string: (input: string) => [input],
|
||||
expression: (input: IJSExpression) => [`{${input.value}}`],
|
||||
node: (input: IComponentNodeItem) => [generateNode(input)],
|
||||
});
|
||||
}
|
||||
|
||||
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
|
||||
const next: ICodeStruct = {
|
||||
...pre,
|
||||
};
|
||||
|
||||
const ir = next.ir as IContainerInfo;
|
||||
|
||||
let jsxContent: string;
|
||||
if (!ir.children || (ir.children as unknown[]).length === 0) {
|
||||
jsxContent = 'null';
|
||||
} else {
|
||||
const childrenCode = generateChildren(ir.children);
|
||||
if (childrenCode.length === 1) {
|
||||
jsxContent = `(${childrenCode[0]})`;
|
||||
} else {
|
||||
jsxContent = `(<React.Fragment>${childrenCode.join(
|
||||
'',
|
||||
)}</React.Fragment>)`;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@ -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;
|
||||
37
packages/code-generator/src/plugins/component/style/css.ts
Normal file
37
packages/code-generator/src/plugins/component/style/css.ts
Normal file
@ -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;
|
||||
54
packages/code-generator/src/plugins/project/constants.ts
Normal file
54
packages/code-generator/src/plugins/project/constants.ts
Normal file
@ -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;
|
||||
@ -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;
|
||||
@ -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: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge,chrome=1" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>${ir.meta.name}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="${ir.config.targetRootID}"></div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
linkAfter: [],
|
||||
});
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@ -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.SCSS,
|
||||
name: COMMON_CHUNK_NAME.StyleDepsImport,
|
||||
content: `
|
||||
// 引入默认全局样式
|
||||
@import '@alifd/next/reset.scss';
|
||||
`,
|
||||
linkAfter: [],
|
||||
});
|
||||
|
||||
next.chunks.push({
|
||||
type: ChunkType.STRING,
|
||||
fileType: FileType.SCSS,
|
||||
name: COMMON_CHUNK_NAME.StyleCssContent,
|
||||
content: `
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
`,
|
||||
linkAfter: [COMMON_CHUNK_NAME.StyleDepsImport],
|
||||
});
|
||||
|
||||
next.chunks.push({
|
||||
type: ChunkType.STRING,
|
||||
fileType: FileType.SCSS,
|
||||
name: COMMON_CHUNK_NAME.StyleCssContent,
|
||||
content: ir.css || '',
|
||||
linkAfter: [COMMON_CHUNK_NAME.StyleDepsImport],
|
||||
});
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@ -0,0 +1,86 @@
|
||||
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: {
|
||||
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;
|
||||
@ -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;
|
||||
@ -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];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -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 (
|
||||
<p className={styles.footer}>
|
||||
<span className={styles.logo}>Alibaba Fusion</span>
|
||||
<br />
|
||||
<span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
return [['src', 'layouts', 'BasicLayout', 'components', 'Footer'], file];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -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 (
|
||||
<div className="logo">
|
||||
<Link to={url || '/'} className={styles.logo}>
|
||||
{image && <img src={image} alt="logo" />}
|
||||
<span>{text}</span>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
return [['src', 'layouts', 'BasicLayout', 'components', 'Logo'], file];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
@ -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 = (
|
||||
<SubNav key={index} icon={item.icon} label={item.name}>
|
||||
{childrenItems}
|
||||
</SubNav>
|
||||
);
|
||||
return subNav;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const navItem = (
|
||||
<NavItem key={item.path} icon={item.icon}>
|
||||
<Link to={item.path}>{item.name}</Link>
|
||||
</NavItem>
|
||||
);
|
||||
return navItem;
|
||||
}
|
||||
|
||||
const Navigation = (props, context) => {
|
||||
const { location } = props;
|
||||
const { pathname } = location;
|
||||
const { isCollapse } = context;
|
||||
return (
|
||||
<Nav
|
||||
type="primary"
|
||||
selectedKeys={[pathname]}
|
||||
defaultSelectedKeys={[pathname]}
|
||||
embeddable
|
||||
openMode="single"
|
||||
iconOnly={isCollapse}
|
||||
hasArrow={false}
|
||||
mode={isCollapse ? 'popup' : 'inline'}
|
||||
>
|
||||
{getNavMenuItems(asideMenuConfig)}
|
||||
</Nav>
|
||||
);
|
||||
};
|
||||
|
||||
Navigation.contextTypes = {
|
||||
isCollapse: PropTypes.bool,
|
||||
};
|
||||
const PageNav = withRouter(Navigation);
|
||||
export default PageNav;
|
||||
`,
|
||||
);
|
||||
|
||||
return [['src', 'layouts', 'BasicLayout', 'components', 'PageNav'], file];
|
||||
}
|
||||
@ -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 (
|
||||
<ConfigProvider device={device}>
|
||||
<Shell
|
||||
type="dark"
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<Shell.Branding>
|
||||
<Logo
|
||||
image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png"
|
||||
text="Logo"
|
||||
/>
|
||||
</Shell.Branding>
|
||||
<Shell.Navigation
|
||||
direction="hoz"
|
||||
style={{
|
||||
marginRight: 10,
|
||||
}}
|
||||
></Shell.Navigation>
|
||||
<Shell.Action></Shell.Action>
|
||||
<Shell.Navigation>
|
||||
<PageNav />
|
||||
</Shell.Navigation>
|
||||
|
||||
<Shell.Content>{children}</Shell.Content>
|
||||
<Shell.Footer>
|
||||
<Footer />
|
||||
</Shell.Footer>
|
||||
</Shell>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
return [['src', 'layouts', 'BasicLayout'], file];
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import ResultFile from '@/model/ResultFile';
|
||||
import { IResultFile } from '@/types';
|
||||
|
||||
export default function getFile(): [string[], IResultFile] {
|
||||
const file = new ResultFile(
|
||||
'menuConfig',
|
||||
'js',
|
||||
`
|
||||
const headerMenuConfig = [];
|
||||
const asideMenuConfig = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
path: '/',
|
||||
icon: 'smile',
|
||||
},
|
||||
];
|
||||
export { headerMenuConfig, asideMenuConfig };
|
||||
`,
|
||||
);
|
||||
|
||||
return [['src', 'layouts', 'BasicLayout'], file];
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import ResultFile from '@/model/ResultFile';
|
||||
import { IResultFile } from '@/types';
|
||||
|
||||
export default function getFile(): [string[], IResultFile] {
|
||||
const file = new ResultFile(
|
||||
'.stylelintignore',
|
||||
'',
|
||||
`
|
||||
# 忽略目录
|
||||
build/
|
||||
tests/
|
||||
demo/
|
||||
|
||||
# node 覆盖率文件
|
||||
coverage/
|
||||
`,
|
||||
);
|
||||
|
||||
return [[], file];
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import ResultFile from '@/model/ResultFile';
|
||||
import { IResultFile } from '@/types';
|
||||
|
||||
export default function getFile(): [string[], IResultFile] {
|
||||
const file = new ResultFile(
|
||||
'.stylelintrc',
|
||||
'js',
|
||||
`
|
||||
const { stylelint } = require('@ice/spec');
|
||||
|
||||
module.exports = stylelint;
|
||||
`,
|
||||
);
|
||||
|
||||
return [[], file];
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import ResultFile from '@/model/ResultFile';
|
||||
import { IResultFile } from '@/types';
|
||||
|
||||
export default function getFile(): [string[], IResultFile] {
|
||||
const file = new ResultFile(
|
||||
'tsconfig',
|
||||
'json',
|
||||
`
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"buildOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"module": "esnext",
|
||||
"target": "es6",
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": ["es6", "dom"],
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"rootDir": "./",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": false,
|
||||
"importHelpers": true,
|
||||
"strictNullChecks": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"noUnusedLocals": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"ice": [".ice/index.ts"],
|
||||
"ice/*": [".ice/pages/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/*", ".ice"],
|
||||
"exclude": ["node_modules", "build", "public"]
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
return [[], file];
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
import ResultDir from '@/model/ResultDir';
|
||||
import { IProjectTemplate, IResultDir, IResultFile } from '@/types';
|
||||
|
||||
import file12 from './files/abc.json';
|
||||
import file11 from './files/build.json';
|
||||
import file10 from './files/editorconfig';
|
||||
import file9 from './files/eslintignore';
|
||||
import file8 from './files/eslintrc.js';
|
||||
import file7 from './files/gitignore';
|
||||
import file6 from './files/jsconfig.json';
|
||||
import file5 from './files/prettierignore';
|
||||
import file4 from './files/prettierrc.js';
|
||||
import file13 from './files/README.md';
|
||||
import file16 from './files/src/layouts/BasicLayout/components/Footer/index.jsx';
|
||||
import file17 from './files/src/layouts/BasicLayout/components/Footer/index.module.scss';
|
||||
import file18 from './files/src/layouts/BasicLayout/components/Logo/index.jsx';
|
||||
import file19 from './files/src/layouts/BasicLayout/components/Logo/index.module.scss';
|
||||
import file20 from './files/src/layouts/BasicLayout/components/PageNav/index.jsx';
|
||||
import file14 from './files/src/layouts/BasicLayout/index.jsx';
|
||||
import file15 from './files/src/layouts/BasicLayout/menuConfig.js';
|
||||
import file3 from './files/stylelintignore';
|
||||
import file2 from './files/stylelintrc.js';
|
||||
import file1 from './files/tsconfig.json';
|
||||
|
||||
type FuncFileGenerator = () => [string[], IResultFile];
|
||||
|
||||
function insertFile(root: IResultDir, path: string[], file: IResultFile) {
|
||||
let current: IResultDir = root;
|
||||
path.forEach(pathNode => {
|
||||
const dir = current.dirs.find(d => d.name === pathNode);
|
||||
if (dir) {
|
||||
current = dir;
|
||||
} else {
|
||||
const newDir = new ResultDir(pathNode);
|
||||
current.addDirectory(newDir);
|
||||
current = newDir;
|
||||
}
|
||||
});
|
||||
|
||||
current.addFile(file);
|
||||
}
|
||||
|
||||
function runFileGenerator(root: IResultDir, fun: FuncFileGenerator) {
|
||||
const [path, file] = fun();
|
||||
insertFile(root, path, file);
|
||||
}
|
||||
|
||||
const icejsTemplate: IProjectTemplate = {
|
||||
slots: {
|
||||
components: {
|
||||
path: ['src', 'components'],
|
||||
},
|
||||
pages: {
|
||||
path: ['src', 'pages'],
|
||||
},
|
||||
router: {
|
||||
path: ['src'],
|
||||
fileName: 'routes',
|
||||
},
|
||||
entry: {
|
||||
path: ['src'],
|
||||
fileName: 'app',
|
||||
},
|
||||
constants: {
|
||||
path: ['src'],
|
||||
fileName: 'constants',
|
||||
},
|
||||
utils: {
|
||||
path: ['src'],
|
||||
fileName: 'utils',
|
||||
},
|
||||
i18n: {
|
||||
path: ['src'],
|
||||
fileName: 'i18n',
|
||||
},
|
||||
globalStyle: {
|
||||
path: ['src'],
|
||||
fileName: 'global',
|
||||
},
|
||||
htmlEntry: {
|
||||
path: ['public'],
|
||||
fileName: 'index',
|
||||
},
|
||||
packageJSON: {
|
||||
path: [],
|
||||
fileName: 'package',
|
||||
},
|
||||
},
|
||||
|
||||
generateTemplate(): IResultDir {
|
||||
const root = new ResultDir('.');
|
||||
|
||||
runFileGenerator(root, file1);
|
||||
runFileGenerator(root, file2);
|
||||
runFileGenerator(root, file3);
|
||||
runFileGenerator(root, file4);
|
||||
runFileGenerator(root, file5);
|
||||
runFileGenerator(root, file6);
|
||||
runFileGenerator(root, file7);
|
||||
runFileGenerator(root, file8);
|
||||
runFileGenerator(root, file9);
|
||||
runFileGenerator(root, file10);
|
||||
runFileGenerator(root, file11);
|
||||
runFileGenerator(root, file12);
|
||||
runFileGenerator(root, file13);
|
||||
runFileGenerator(root, file14);
|
||||
runFileGenerator(root, file15);
|
||||
runFileGenerator(root, file16);
|
||||
runFileGenerator(root, file17);
|
||||
runFileGenerator(root, file18);
|
||||
runFileGenerator(root, file19);
|
||||
runFileGenerator(root, file20);
|
||||
|
||||
return root;
|
||||
},
|
||||
};
|
||||
|
||||
export default icejsTemplate;
|
||||
66
packages/code-generator/src/plugins/project/i18n.ts
Normal file
66
packages/code-generator/src/plugins/project/i18n.ts
Normal file
@ -0,0 +1,66 @@
|
||||
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.i18n) {
|
||||
const [, i18nStr] = generateCompositeType(ir.i18n);
|
||||
|
||||
next.chunks.push({
|
||||
type: ChunkType.STRING,
|
||||
fileType: FileType.JS,
|
||||
name: COMMON_CHUNK_NAME.FileMainContent,
|
||||
content: `
|
||||
const i18nConfig = ${i18nStr};
|
||||
let locale = 'en_US';
|
||||
|
||||
const changeLocale = (target) => {
|
||||
locale = target;
|
||||
};
|
||||
|
||||
const i18n = key => i18nConfig && i18nConfig[locale] && i18nConfig[locale][key] || '';
|
||||
`,
|
||||
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.JS,
|
||||
name: COMMON_CHUNK_NAME.FileExport,
|
||||
content: `
|
||||
export {
|
||||
changeLocale,
|
||||
i18n,
|
||||
};
|
||||
`,
|
||||
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;
|
||||
90
packages/code-generator/src/plugins/project/utils.ts
Normal file
90
packages/code-generator/src/plugins/project/utils.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { COMMON_CHUNK_NAME } from '@/const/generator';
|
||||
import { generateCompositeType } from '@/plugins/utils/compositeType';
|
||||
// import { } from '@/plugins/utils/jsExpression';
|
||||
import {
|
||||
BuilderComponentPlugin,
|
||||
ChunkType,
|
||||
FileType,
|
||||
ICodeStruct,
|
||||
IUtilInfo,
|
||||
} 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 IUtilInfo;
|
||||
|
||||
if (ir.utils) {
|
||||
next.chunks.push({
|
||||
type: ChunkType.STRING,
|
||||
fileType: FileType.JS,
|
||||
name: COMMON_CHUNK_NAME.FileExport,
|
||||
content: `
|
||||
export default {
|
||||
`,
|
||||
linkAfter: [
|
||||
COMMON_CHUNK_NAME.ExternalDepsImport,
|
||||
COMMON_CHUNK_NAME.InternalDepsImport,
|
||||
COMMON_CHUNK_NAME.FileVarDefine,
|
||||
COMMON_CHUNK_NAME.FileUtilDefine,
|
||||
COMMON_CHUNK_NAME.FileMainContent,
|
||||
],
|
||||
});
|
||||
|
||||
ir.utils.forEach(util => {
|
||||
if (util.type === 'function') {
|
||||
next.chunks.push({
|
||||
type: ChunkType.STRING,
|
||||
fileType: FileType.JS,
|
||||
name: COMMON_CHUNK_NAME.FileVarDefine,
|
||||
content: `
|
||||
const ${util.name} = ${util.content};
|
||||
`,
|
||||
linkAfter: [
|
||||
COMMON_CHUNK_NAME.ExternalDepsImport,
|
||||
COMMON_CHUNK_NAME.InternalDepsImport,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
next.chunks.push({
|
||||
type: ChunkType.STRING,
|
||||
fileType: FileType.JS,
|
||||
name: COMMON_CHUNK_NAME.FileExport,
|
||||
content: `
|
||||
${util.name},
|
||||
`,
|
||||
linkAfter: [
|
||||
COMMON_CHUNK_NAME.ExternalDepsImport,
|
||||
COMMON_CHUNK_NAME.InternalDepsImport,
|
||||
COMMON_CHUNK_NAME.FileVarDefine,
|
||||
COMMON_CHUNK_NAME.FileUtilDefine,
|
||||
COMMON_CHUNK_NAME.FileMainContent,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
next.chunks.push({
|
||||
type: ChunkType.STRING,
|
||||
fileType: FileType.JS,
|
||||
name: COMMON_CHUNK_NAME.FileExport,
|
||||
content: `
|
||||
};
|
||||
`,
|
||||
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;
|
||||
45
packages/code-generator/src/plugins/utils/compositeType.ts
Normal file
45
packages/code-generator/src/plugins/utils/compositeType.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { CompositeArray, CompositeValue, ICompositeObject } from '@/types';
|
||||
import { generateValue, isJsExpression } from './jsExpression';
|
||||
|
||||
function generateArray(value: CompositeArray): string {
|
||||
const body = value.map(v => generateUnknownType(v)).join(',');
|
||||
return `[${body}]`;
|
||||
}
|
||||
|
||||
function generateObject(value: ICompositeObject): string {
|
||||
if (isJsExpression(value)) {
|
||||
return generateValue(value);
|
||||
}
|
||||
|
||||
const body = Object.keys(value)
|
||||
.map(key => {
|
||||
const v = generateUnknownType(value[key]);
|
||||
return `${key}: ${v}`;
|
||||
})
|
||||
.join(',');
|
||||
|
||||
return `{${body}}`;
|
||||
}
|
||||
|
||||
function generateUnknownType(value: CompositeValue): string {
|
||||
if (Array.isArray(value)) {
|
||||
return generateArray(value as CompositeArray);
|
||||
} else if (typeof value === 'object') {
|
||||
return generateObject(value as ICompositeObject);
|
||||
} else if (typeof value === 'string') {
|
||||
return `'${value}'`;
|
||||
}
|
||||
return `${value}`;
|
||||
}
|
||||
|
||||
export function generateCompositeType(
|
||||
value: CompositeValue,
|
||||
): [boolean, string] {
|
||||
const result = generateUnknownType(value);
|
||||
|
||||
if (result.substr(0, 1) === "'" && result.substr(-1, 1) === "'") {
|
||||
return [true, result.substring(1, result.length - 1)];
|
||||
}
|
||||
|
||||
return [false, result];
|
||||
}
|
||||
39
packages/code-generator/src/plugins/utils/jsExpression.ts
Normal file
39
packages/code-generator/src/plugins/utils/jsExpression.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { CodeGeneratorError, IJSExpression } from '../../types';
|
||||
|
||||
export function transformFuncExpr2MethodMember(
|
||||
methodName: string,
|
||||
functionBody: string,
|
||||
): string {
|
||||
if (functionBody.indexOf('function') < 8) {
|
||||
return functionBody.replace('function', methodName);
|
||||
}
|
||||
return functionBody;
|
||||
}
|
||||
|
||||
export function getFuncExprBody(functionBody: string) {
|
||||
const start = functionBody.indexOf('{');
|
||||
const end = functionBody.lastIndexOf('}');
|
||||
|
||||
if (start < 0 || end < 0 || end < start) {
|
||||
throw new CodeGeneratorError('JSExpression has no valid body.');
|
||||
}
|
||||
|
||||
const body = functionBody.slice(start + 1, end);
|
||||
return body;
|
||||
}
|
||||
|
||||
export function generateValue(value: any): string {
|
||||
if (value && (value as IJSExpression).type === 'JSExpression') {
|
||||
return (value as IJSExpression).value;
|
||||
}
|
||||
|
||||
throw new CodeGeneratorError('Not a JSExpression');
|
||||
}
|
||||
|
||||
export function isJsExpression(value: any): boolean {
|
||||
return (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
(value as IJSExpression).type === 'JSExpression'
|
||||
);
|
||||
}
|
||||
89
packages/code-generator/src/publisher/disk/index.ts
Normal file
89
packages/code-generator/src/publisher/disk/index.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { CodeGeneratorError, IResultDir } from '@/types';
|
||||
|
||||
export type PublisherFactory<T, U> = (configuration?: Partial<T>) => U;
|
||||
|
||||
export interface IPublisher<T, U> {
|
||||
publish: (options?: T) => Promise<IPublisherResponse<U>>;
|
||||
getProject: () => IResultDir | void;
|
||||
setProject: (project: IResultDir) => void;
|
||||
}
|
||||
|
||||
export interface IPublisherFactoryParams {
|
||||
project?: IResultDir;
|
||||
}
|
||||
export interface IPublisherResponse<T> {
|
||||
success: boolean;
|
||||
payload?: T;
|
||||
}
|
||||
|
||||
import { writeFolder } from './utils';
|
||||
|
||||
export interface IDiskFactoryParams extends IPublisherFactoryParams {
|
||||
outputPath?: string;
|
||||
projectSlug?: string;
|
||||
createProjectFolder?: boolean;
|
||||
}
|
||||
|
||||
export interface IDiskPublisher extends IPublisher<IDiskFactoryParams, string> {
|
||||
getOutputPath: () => string;
|
||||
setOutputPath: (path: string) => void;
|
||||
}
|
||||
|
||||
export const createDiskPublisher: PublisherFactory<
|
||||
IDiskFactoryParams,
|
||||
IDiskPublisher
|
||||
> = (params: IDiskFactoryParams = {}): IDiskPublisher => {
|
||||
let { project, outputPath = './' } = params;
|
||||
|
||||
const getProject = (): IResultDir => {
|
||||
if (!project) {
|
||||
throw new CodeGeneratorError('Missing Project');
|
||||
}
|
||||
return project;
|
||||
};
|
||||
const setProject = (projectToSet: IResultDir): void => {
|
||||
project = projectToSet;
|
||||
};
|
||||
|
||||
const getOutputPath = (): string => {
|
||||
return outputPath;
|
||||
};
|
||||
const setOutputPath = (path: string): void => {
|
||||
outputPath = path;
|
||||
};
|
||||
|
||||
const publish = async (options: IDiskFactoryParams = {}) => {
|
||||
const projectToPublish = options.project || project;
|
||||
if (!projectToPublish) {
|
||||
throw new CodeGeneratorError('Missing Project');
|
||||
}
|
||||
|
||||
const projectOutputPath = options.outputPath || outputPath;
|
||||
const overrideProjectSlug = options.projectSlug || params.projectSlug;
|
||||
const createProjectFolder =
|
||||
options.createProjectFolder || params.createProjectFolder;
|
||||
|
||||
if (overrideProjectSlug) {
|
||||
projectToPublish.name = overrideProjectSlug;
|
||||
}
|
||||
|
||||
try {
|
||||
await writeFolder(
|
||||
projectToPublish,
|
||||
projectOutputPath,
|
||||
createProjectFolder,
|
||||
);
|
||||
return { success: true, payload: projectOutputPath };
|
||||
} catch (error) {
|
||||
throw new CodeGeneratorError(error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
publish,
|
||||
getProject,
|
||||
setProject,
|
||||
getOutputPath,
|
||||
setOutputPath,
|
||||
};
|
||||
};
|
||||
71
packages/code-generator/src/publisher/disk/utils.ts
Normal file
71
packages/code-generator/src/publisher/disk/utils.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { existsSync, mkdir, writeFile } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { IResultDir, IResultFile } from '@/types';
|
||||
|
||||
export const writeFolder = async (
|
||||
folder: IResultDir,
|
||||
currentPath: string,
|
||||
createProjectFolder = true,
|
||||
): Promise<void> => {
|
||||
const { name, files, dirs } = folder;
|
||||
|
||||
const folderPath = createProjectFolder
|
||||
? join(currentPath, name)
|
||||
: currentPath;
|
||||
|
||||
if (!existsSync(folderPath)) {
|
||||
await createDirectory(folderPath);
|
||||
}
|
||||
|
||||
const promises = [
|
||||
writeFilesToFolder(folderPath, files),
|
||||
writeSubFoldersToFolder(folderPath, dirs),
|
||||
];
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
const writeFilesToFolder = async (
|
||||
folderPath: string,
|
||||
files: IResultFile[],
|
||||
): Promise<void> => {
|
||||
const promises = files.map(file => {
|
||||
const fileName = file.ext ? `${file.name}.${file.ext}` : file.name;
|
||||
const filePath = join(folderPath, fileName);
|
||||
return writeContentToFile(filePath, file.content);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
const writeSubFoldersToFolder = async (
|
||||
folderPath: string,
|
||||
subFolders: IResultDir[],
|
||||
): Promise<void> => {
|
||||
const promises = subFolders.map(subFolder => {
|
||||
return writeFolder(subFolder, folderPath);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
const createDirectory = (pathToDir: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
mkdir(pathToDir, { recursive: true }, err => {
|
||||
err ? reject(err) : resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const writeContentToFile = (
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
encoding: string = 'utf8',
|
||||
): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
writeFile(filePath, fileContent, encoding, err => {
|
||||
err ? reject(err) : resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
60
packages/code-generator/src/solutions/icejs.ts
Normal file
60
packages/code-generator/src/solutions/icejs.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { IProjectBuilder } from '@/types';
|
||||
|
||||
import { createProjectBuilder } from '@/generator/ProjectBuilder';
|
||||
|
||||
import esmodule from '@/plugins/common/esmodule';
|
||||
import containerClass from '@/plugins/component/react/containerClass';
|
||||
import containerInitState from '@/plugins/component/react/containerInitState';
|
||||
// import containerInjectUtils from '@/plugins/component/react/containerInjectUtils';
|
||||
import containerLifeCycle from '@/plugins/component/react/containerLifeCycle';
|
||||
import containerMethod from '@/plugins/component/react/containerMethod';
|
||||
import jsx from '@/plugins/component/react/jsx';
|
||||
import reactCommonDeps from '@/plugins/component/react/reactCommonDeps';
|
||||
import css from '@/plugins/component/style/css';
|
||||
import constants from '@/plugins/project/constants';
|
||||
import iceJsEntry from '@/plugins/project/framework/icejs/plugins/entry';
|
||||
import iceJsEntryHtml from '@/plugins/project/framework/icejs/plugins/entryHtml';
|
||||
import iceJsGlobalStyle from '@/plugins/project/framework/icejs/plugins/globalStyle';
|
||||
import iceJsPackageJSON from '@/plugins/project/framework/icejs/plugins/packageJSON';
|
||||
import iceJsRouter from '@/plugins/project/framework/icejs/plugins/router';
|
||||
import template from '@/plugins/project/framework/icejs/template';
|
||||
import i18n from '@/plugins/project/i18n';
|
||||
import utils from '@/plugins/project/utils';
|
||||
|
||||
export default function createIceJsProjectBuilder(): IProjectBuilder {
|
||||
return createProjectBuilder({
|
||||
template,
|
||||
plugins: {
|
||||
components: [
|
||||
reactCommonDeps,
|
||||
esmodule,
|
||||
containerClass,
|
||||
// containerInjectUtils,
|
||||
containerInitState,
|
||||
containerLifeCycle,
|
||||
containerMethod,
|
||||
jsx,
|
||||
css,
|
||||
],
|
||||
pages: [
|
||||
reactCommonDeps,
|
||||
esmodule,
|
||||
containerClass,
|
||||
// containerInjectUtils,
|
||||
containerInitState,
|
||||
containerLifeCycle,
|
||||
containerMethod,
|
||||
jsx,
|
||||
css,
|
||||
],
|
||||
router: [esmodule, iceJsRouter],
|
||||
entry: [iceJsEntry],
|
||||
constants: [constants],
|
||||
utils: [esmodule, utils],
|
||||
i18n: [i18n],
|
||||
globalStyle: [iceJsGlobalStyle],
|
||||
htmlEntry: [iceJsEntryHtml],
|
||||
packageJSON: [iceJsPackageJSON],
|
||||
},
|
||||
});
|
||||
}
|
||||
143
packages/code-generator/src/types/core.ts
Normal file
143
packages/code-generator/src/types/core.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import {
|
||||
IBasicSchema,
|
||||
IParseResult,
|
||||
IProjectSchema,
|
||||
IResultDir,
|
||||
IResultFile,
|
||||
} from './index';
|
||||
|
||||
export enum FileType {
|
||||
CSS = 'css',
|
||||
SCSS = 'scss',
|
||||
HTML = 'html',
|
||||
JS = 'js',
|
||||
JSX = 'jsx',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
export enum ChunkType {
|
||||
AST = 'ast',
|
||||
STRING = 'string',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
export enum PluginType {
|
||||
COMPONENT = 'component',
|
||||
UTILS = 'utils',
|
||||
I18N = 'i18n',
|
||||
}
|
||||
|
||||
export type ChunkContent = string | any;
|
||||
export type CodeGeneratorFunction<T> = (content: T) => string;
|
||||
|
||||
export interface ICodeChunk {
|
||||
type: ChunkType;
|
||||
fileType: FileType;
|
||||
name: string;
|
||||
subModule?: string;
|
||||
content: ChunkContent;
|
||||
linkAfter: string[];
|
||||
}
|
||||
|
||||
export interface IBaseCodeStruct {
|
||||
chunks: ICodeChunk[];
|
||||
depNames: string[];
|
||||
}
|
||||
|
||||
export interface ICodeStruct extends IBaseCodeStruct {
|
||||
ir: any;
|
||||
chunks: ICodeChunk[];
|
||||
}
|
||||
|
||||
export type BuilderComponentPlugin = (
|
||||
initStruct: ICodeStruct,
|
||||
) => Promise<ICodeStruct>;
|
||||
|
||||
export interface IChunkBuilder {
|
||||
run(
|
||||
ir: any,
|
||||
initialStructure?: ICodeStruct,
|
||||
): Promise<{ chunks: ICodeChunk[][] }>;
|
||||
getPlugins(): BuilderComponentPlugin[];
|
||||
addPlugin(plugin: BuilderComponentPlugin): void;
|
||||
}
|
||||
|
||||
export interface ICodeBuilder {
|
||||
link(chunkDefinitions: ICodeChunk[]): string;
|
||||
generateByType(type: string, content: unknown): string;
|
||||
}
|
||||
|
||||
export interface ICompiledModule {
|
||||
files: IResultFile[];
|
||||
}
|
||||
|
||||
export interface IModuleBuilder {
|
||||
generateModule: (input: unknown) => Promise<ICompiledModule>;
|
||||
linkCodeChunks: (
|
||||
chunks: Record<string, ICodeChunk[]>,
|
||||
fileName: string,
|
||||
) => IResultFile[];
|
||||
addPlugin: (plugin: BuilderComponentPlugin) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 引擎对外接口
|
||||
*
|
||||
* @export
|
||||
* @interface ICodeGenerator
|
||||
*/
|
||||
export interface ICodeGenerator {
|
||||
/**
|
||||
* 出码接口,把 Schema 转换成代码文件系统描述
|
||||
*
|
||||
* @param {(IBasicSchema)} schema 传入的 Schema
|
||||
* @returns {IResultDir}
|
||||
* @memberof ICodeGenerator
|
||||
*/
|
||||
toCode(schema: IBasicSchema): Promise<IResultDir>;
|
||||
}
|
||||
|
||||
export interface ISchemaParser {
|
||||
validate(schema: IBasicSchema): boolean;
|
||||
parse(schema: IBasicSchema): IParseResult;
|
||||
}
|
||||
|
||||
export interface IProjectTemplate {
|
||||
slots: IProjectSlots;
|
||||
generateTemplate(): IResultDir;
|
||||
}
|
||||
|
||||
export interface IProjectSlot {
|
||||
path: string[];
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export interface IProjectSlots {
|
||||
components: IProjectSlot;
|
||||
pages: IProjectSlot;
|
||||
router: IProjectSlot;
|
||||
entry: IProjectSlot;
|
||||
constants?: IProjectSlot;
|
||||
utils?: IProjectSlot;
|
||||
i18n?: IProjectSlot;
|
||||
globalStyle: IProjectSlot;
|
||||
htmlEntry: IProjectSlot;
|
||||
packageJSON: IProjectSlot;
|
||||
}
|
||||
|
||||
export interface IProjectPlugins {
|
||||
components: BuilderComponentPlugin[];
|
||||
pages: BuilderComponentPlugin[];
|
||||
router: BuilderComponentPlugin[];
|
||||
entry: BuilderComponentPlugin[];
|
||||
constants?: BuilderComponentPlugin[];
|
||||
utils?: BuilderComponentPlugin[];
|
||||
i18n?: BuilderComponentPlugin[];
|
||||
globalStyle: BuilderComponentPlugin[];
|
||||
htmlEntry: BuilderComponentPlugin[];
|
||||
packageJSON: BuilderComponentPlugin[];
|
||||
}
|
||||
|
||||
export interface IProjectBuilder {
|
||||
generateProject(schema: IProjectSchema): Promise<IResultDir>;
|
||||
}
|
||||
36
packages/code-generator/src/types/deps.ts
Normal file
36
packages/code-generator/src/types/deps.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 外部依赖描述
|
||||
*
|
||||
* @export
|
||||
* @interface IExternalDependency
|
||||
*/
|
||||
export interface IExternalDependency extends IDependency {
|
||||
package: string; // 组件包的名称
|
||||
version: string; // 组件包的版本
|
||||
}
|
||||
|
||||
export enum InternalDependencyType {
|
||||
PAGE = 'pages',
|
||||
BLOCK = 'components',
|
||||
COMPONENT = 'components',
|
||||
UTILS = 'utils',
|
||||
}
|
||||
|
||||
export enum DependencyType {
|
||||
External = 'External',
|
||||
Internal = 'Internal',
|
||||
}
|
||||
|
||||
export interface IInternalDependency extends IDependency {
|
||||
type: InternalDependencyType;
|
||||
moduleName: string;
|
||||
}
|
||||
|
||||
export interface IDependency {
|
||||
destructuring: boolean; // 组件是否是解构方式方式导出
|
||||
exportName: string; // 导出命名
|
||||
subName?: string; // 下标子组件名称
|
||||
main?: string; // 包导出组件入口文件路径 /lib/input
|
||||
dependencyType?: DependencyType; // 依赖类型 内/外
|
||||
importName?: string; // 导入后名称
|
||||
}
|
||||
20
packages/code-generator/src/types/error.ts
Normal file
20
packages/code-generator/src/types/error.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export class CodeGeneratorError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: max-classes-per-file
|
||||
export class ComponentValidationError extends CodeGeneratorError {
|
||||
constructor(errorString: string) {
|
||||
super(errorString);
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: max-classes-per-file
|
||||
export class CompatibilityError extends CodeGeneratorError {
|
||||
constructor(errorString: string) {
|
||||
super(errorString);
|
||||
}
|
||||
}
|
||||
6
packages/code-generator/src/types/index.ts
Normal file
6
packages/code-generator/src/types/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './core';
|
||||
export * from './deps';
|
||||
export * from './error';
|
||||
export * from './result';
|
||||
export * from './schema';
|
||||
export * from './intermediate';
|
||||
64
packages/code-generator/src/types/intermediate.ts
Normal file
64
packages/code-generator/src/types/intermediate.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import {
|
||||
IAppConfig,
|
||||
IAppMeta,
|
||||
IContainerNodeItem,
|
||||
IDependency,
|
||||
II18nMap,
|
||||
IInternalDependency,
|
||||
IUtilItem,
|
||||
} from './index';
|
||||
|
||||
export interface IParseResult {
|
||||
containers: IContainerInfo[];
|
||||
globalUtils?: IUtilInfo;
|
||||
globalI18n?: II18nMap;
|
||||
globalRouter?: IRouterInfo;
|
||||
project?: IProjectInfo;
|
||||
}
|
||||
|
||||
export interface IContainerInfo extends IContainerNodeItem, IWithDependency {
|
||||
componentName: string;
|
||||
containerType: string;
|
||||
}
|
||||
|
||||
export interface IWithDependency {
|
||||
deps?: IDependency[];
|
||||
}
|
||||
|
||||
export interface IUtilInfo extends IWithDependency {
|
||||
utils: IUtilItem[];
|
||||
}
|
||||
|
||||
export interface IRouterInfo extends IWithDependency {
|
||||
routes: Array<{
|
||||
path: string;
|
||||
componentName: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IProjectInfo {
|
||||
config: IAppConfig;
|
||||
meta: IAppMeta;
|
||||
css?: string;
|
||||
constants?: Record<string, string>;
|
||||
i18n?: II18nMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* From meta
|
||||
* page title
|
||||
* router
|
||||
* spmb
|
||||
*
|
||||
* Utils
|
||||
*
|
||||
* constants
|
||||
*
|
||||
* i18n
|
||||
*
|
||||
* components
|
||||
*
|
||||
* pages
|
||||
*
|
||||
* layout
|
||||
*/
|
||||
88
packages/code-generator/src/types/result.ts
Normal file
88
packages/code-generator/src/types/result.ts
Normal file
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 导出内容结构,文件夹
|
||||
*
|
||||
* @export
|
||||
* @interface IResultDir
|
||||
*/
|
||||
export interface IResultDir {
|
||||
/**
|
||||
* 文件夹名称,Root 名称默认为 .
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof IResultDir
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 子目录
|
||||
*
|
||||
* @type {IResultDir[]}
|
||||
* @memberof IResultDir
|
||||
*/
|
||||
dirs: IResultDir[];
|
||||
/**
|
||||
* 文件夹内文件
|
||||
*
|
||||
* @type {IResultFile[]}
|
||||
* @memberof IResultDir
|
||||
*/
|
||||
files: IResultFile[];
|
||||
/**
|
||||
* 添加文件
|
||||
*
|
||||
* @param {IResultFile} file
|
||||
* @memberof IResultDir
|
||||
*/
|
||||
addFile(file: IResultFile): void;
|
||||
/**
|
||||
* 添加子目录
|
||||
*
|
||||
* @param {IResultDir} dir
|
||||
* @memberof IResultDir
|
||||
*/
|
||||
addDirectory(dir: IResultDir): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出内容,对文件的描述
|
||||
*
|
||||
* @export
|
||||
* @interface IResultFile
|
||||
*/
|
||||
export interface IResultFile {
|
||||
/**
|
||||
* 文件名
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof IResultFile
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 文件类型扩展名,例如 .js .less
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof IResultFile
|
||||
*/
|
||||
ext: string;
|
||||
/**
|
||||
* 文件内容
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof IResultFile
|
||||
*/
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface IPackageJSON {
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
dependencies: Record<string, string>;
|
||||
devDependencies: Record<string, string>;
|
||||
scripts?: Record<string, string>;
|
||||
engines?: Record<string, string>;
|
||||
repository?: {
|
||||
type: string;
|
||||
url: string;
|
||||
};
|
||||
private?: boolean;
|
||||
}
|
||||
255
packages/code-generator/src/types/schema.ts
Normal file
255
packages/code-generator/src/types/schema.ts
Normal file
@ -0,0 +1,255 @@
|
||||
// 搭建基础协议、搭建入料协议的数据规范
|
||||
import { IExternalDependency } from './index';
|
||||
|
||||
/**
|
||||
* 搭建基础协议 - 函数表达式
|
||||
*
|
||||
* @export
|
||||
* @interface IJSExpression
|
||||
*/
|
||||
export interface IJSExpression {
|
||||
type: 'JSExpression';
|
||||
value: string;
|
||||
}
|
||||
|
||||
// JSON 基本类型
|
||||
export interface IJSONObject {
|
||||
[key: string]: JSONValue;
|
||||
}
|
||||
|
||||
export type JSONValue =
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
| null
|
||||
| JSONArray
|
||||
| IJSONObject;
|
||||
export type JSONArray = JSONValue[];
|
||||
|
||||
export type CompositeArray = CompositeValue[];
|
||||
export interface ICompositeObject {
|
||||
[key: string]: CompositeValue;
|
||||
}
|
||||
|
||||
// 复合类型
|
||||
export type CompositeValue =
|
||||
| JSONValue
|
||||
| IJSExpression
|
||||
| CompositeArray
|
||||
| ICompositeObject;
|
||||
|
||||
/**
|
||||
* 搭建基础协议 - 多语言描述
|
||||
*
|
||||
* @export
|
||||
* @interface II18nMap
|
||||
*/
|
||||
export interface II18nMap {
|
||||
[lang: string]: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 搭建基础协议
|
||||
*
|
||||
* @export
|
||||
* @interface IBasicSchema
|
||||
*/
|
||||
export interface IBasicSchema {
|
||||
version: string; // 当前协议版本号
|
||||
componentsMap: IComponentsMapItem[]; // 组件映射关系
|
||||
componentsTree: Array<IContainerNodeItem | IComponentNodeItem>; // 描述模版/页面/区块/低代码业务组件的组件树 低代码业务组件树描述,固定长度为1,且顶层为低代码业务组件容器描述
|
||||
utils?: IUtilItem[]; // 工具类扩展映射关系 低代码业务组件不包含
|
||||
i18n?: II18nMap; // 国际化语料
|
||||
}
|
||||
|
||||
export interface IProjectSchema extends IBasicSchema {
|
||||
constants: Record<string, string>; // 应用范围内的全局常量;
|
||||
css: string; // 应用范围内的全局样式;
|
||||
config: IAppConfig; // 当前应用配置信息
|
||||
meta: IAppMeta; // 当前应用元数据信息
|
||||
}
|
||||
|
||||
/**
|
||||
* 搭建基础协议 - 单个组件描述
|
||||
*
|
||||
* @export
|
||||
* @interface IComponentsMapItem
|
||||
*/
|
||||
export interface IComponentsMapItem extends IExternalDependency {
|
||||
componentName: string; // 组件名称
|
||||
}
|
||||
|
||||
export interface IUtilItem {
|
||||
name: string;
|
||||
type: 'npm' | 'tnpm' | 'function';
|
||||
content: IExternalDependency | IJSExpression;
|
||||
}
|
||||
|
||||
export interface IInlineStyle {
|
||||
[cssAttribute: string]: string | number | IJSExpression;
|
||||
}
|
||||
|
||||
export type ChildNodeItem = string | IJSExpression | IComponentNodeItem;
|
||||
export type ChildNodeType = ChildNodeItem | ChildNodeItem[];
|
||||
|
||||
/**
|
||||
* 搭建基础协议 - 单个组件树节点描述
|
||||
* 转换成一个 .jsx 文件内 React Class 类 render 函数返回的 jsx 代码
|
||||
*
|
||||
* @export
|
||||
* @interface IComponentNodeItem
|
||||
*/
|
||||
export interface IComponentNodeItem {
|
||||
// TODO: 不需要 id 字段,暂时简单兼容
|
||||
id?: string;
|
||||
componentName: string; // 组件名称 必填、首字母大写
|
||||
props: {
|
||||
className?: string; // 组件样式类名
|
||||
style?: IInlineStyle; // 组件内联样式
|
||||
[propName: string]: any; // 业务属性
|
||||
}; // 组件属性对象
|
||||
condition?: CompositeValue; // 渲染条件
|
||||
loop?: CompositeValue; // 循环数据
|
||||
loopArgs?: [string, string]; // 循环迭代对象、索引名称 ["item", "index"]
|
||||
children?: ChildNodeType; // 子节点
|
||||
}
|
||||
|
||||
/**
|
||||
* 搭建基础协议 - 单个容器节点描述
|
||||
*
|
||||
* @export
|
||||
* @interface IContainerNodeItem
|
||||
* @extends {IComponentNodeItem}
|
||||
*/
|
||||
export interface IContainerNodeItem extends IComponentNodeItem {
|
||||
componentName: string; // 'Page' | 'Block' | 'Component' 组件类型 必填、首字母大写
|
||||
fileName: string; // 文件名称 必填、英文
|
||||
defaultProps?: {
|
||||
[propName: string]: any; // 业务属性
|
||||
};
|
||||
state?: {
|
||||
[stateName: string]: any; // 容器初始数据
|
||||
};
|
||||
css: string; // 样式文件 用于描述容器组件内部节点的样式,对应生成一个独立的样式文件,在对应容器组件生成的 .jsx 文件中 import 引入;
|
||||
/**
|
||||
* LifeCycle
|
||||
* • constructor(props, context)
|
||||
* • 说明:初始化渲染时执行,常用于设置state值;
|
||||
* • render()
|
||||
* • 说明:执行于容器组件React Class的render方法最前,常用于计算变量挂载到this对象上,供props上属性绑定。此render()方法不需要设置return返回值。
|
||||
* • componentDidMount()
|
||||
* • componentDidUpdate(prevProps, prevState, snapshot)
|
||||
* • componentWillUnmount()
|
||||
* • componentDidCatch(error, info)
|
||||
*/
|
||||
lifeCycles?: {
|
||||
constructor?: IJSExpression;
|
||||
render?: IJSExpression;
|
||||
componentDidMount?: IJSExpression;
|
||||
componentDidUpdate?: IJSExpression;
|
||||
componentWillUnmount?: IJSExpression;
|
||||
componentDidCatch?: IJSExpression;
|
||||
}; // 生命周期Hook方法
|
||||
methods?: {
|
||||
[methodName: string]: IJSExpression;
|
||||
}; // 自定义方法设置
|
||||
dataSource?: IDataSource; // 异步数据源配置
|
||||
meta?: IBasicMeta | IPageMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搭建基础协议 - 数据源
|
||||
*
|
||||
* @export
|
||||
* @interface IDataSource
|
||||
*/
|
||||
export interface IDataSource {
|
||||
list: IDataSourceConfig[]; // 成为为单个请求配置
|
||||
/**
|
||||
* 参数:为dataMap对象,key:数据id, value: 单个请求结果
|
||||
* 返回值:数据对象data,将会在渲染引擎和schemaToCode中通过调用this.setState(...)将返回的数据对象生效到state中;
|
||||
* 支持返回一个Promise,通过resolve(返回数据),常用于串型发送请求场景,配合this.dataSourceMap[oneRequest.id].load()使用;
|
||||
*/
|
||||
dataHandler?: IJSExpression;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搭建基础协议 - 数据源单个配置
|
||||
*
|
||||
* @export
|
||||
* @interface IDataSourceConfig
|
||||
*/
|
||||
export interface IDataSourceConfig {
|
||||
id: string; // 数据请求ID标识
|
||||
isInit: boolean; // 是否为初始数据 支持表达式 值为true时,将在组件初始化渲染时自动发送当前数据请求
|
||||
type: 'fetch' | 'mtop' | 'jsonp' | 'custom' | 'doServer'; // 数据请求类型
|
||||
requestHandler?: IJSExpression; // 自定义扩展的外部请求处理器 仅type='custom'时生效
|
||||
options?: IFetchOptions; // 请求参数配置 每种请求类型对应不同参数
|
||||
dataHandler?: IJSExpression; // 数据结果处理函数,形如:(data, err) => Object
|
||||
}
|
||||
|
||||
/**
|
||||
* 搭建基础协议 - 请求参数配置
|
||||
*
|
||||
* @export
|
||||
* @interface IFetchOptions
|
||||
*/
|
||||
export interface IFetchOptions {
|
||||
uri: string; // 请求地址 支持表达式
|
||||
params?: {
|
||||
// 请求参数
|
||||
[key: string]: any;
|
||||
};
|
||||
method: 'GET' | 'POST';
|
||||
isCors?: boolean; // 是否支持跨域,对应credentials = 'include'
|
||||
timeout?: number; // 超时时长
|
||||
headers?: {
|
||||
// 自定义请求头
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IBasicMeta {
|
||||
title: string; // 标题描述
|
||||
}
|
||||
|
||||
export interface IPageMeta extends IBasicMeta {
|
||||
router: string; // 页面路由
|
||||
spmb?: string; // spm
|
||||
}
|
||||
|
||||
// "theme": {
|
||||
// //for Fusion use dpl defined
|
||||
// "package": "@alife/theme-fusion",
|
||||
// "version": "^0.1.0",
|
||||
|
||||
// //for Antd use variable
|
||||
// "primary": "#ff9966"
|
||||
// }
|
||||
|
||||
// "layout": {
|
||||
// "componentName": "BasicLayout",
|
||||
// "props": {
|
||||
// "logo": "...",
|
||||
// "name": "测试网站"
|
||||
// },
|
||||
// },
|
||||
|
||||
export interface IAppConfig {
|
||||
sdkVersion: string; // 渲染模块版本
|
||||
historyMode: 'brower' | 'hash'; // 浏览器路由:brower 哈希路由:hash
|
||||
targetRootID: string; // 渲染根节点 ID
|
||||
layout: IComponentNodeItem;
|
||||
theme: object; // 主题配置,根据接入的主题模块不同
|
||||
}
|
||||
|
||||
export interface IAppMeta {
|
||||
name: string; // 应用中文名称
|
||||
git_group?: string; // 应用对应git分组名
|
||||
project_name?: string; // 应用对应git的project名称
|
||||
description?: string; // 应用描述
|
||||
spma?: string; // 应用spma A位信息
|
||||
creator?: string; // author
|
||||
}
|
||||
35
packages/code-generator/src/utils/children.ts
Normal file
35
packages/code-generator/src/utils/children.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {
|
||||
ChildNodeItem,
|
||||
ChildNodeType,
|
||||
IComponentNodeItem,
|
||||
IJSExpression,
|
||||
} from '@/types';
|
||||
|
||||
// tslint:disable-next-line: no-empty
|
||||
const noop = () => [];
|
||||
|
||||
export function handleChildren<T>(
|
||||
children: ChildNodeType,
|
||||
handlers: {
|
||||
string?: (input: string) => T[];
|
||||
expression?: (input: IJSExpression) => T[];
|
||||
node?: (input: IComponentNodeItem) => T[];
|
||||
common?: (input: unknown) => T[];
|
||||
},
|
||||
): T[] {
|
||||
if (Array.isArray(children)) {
|
||||
const list: ChildNodeItem[] = children as ChildNodeItem[];
|
||||
return list
|
||||
.map(child => handleChildren(child, handlers))
|
||||
.reduce((p, c) => p.concat(c), []);
|
||||
} else if (typeof children === 'string') {
|
||||
const handler = handlers.string || handlers.common || noop;
|
||||
return handler(children as string);
|
||||
} else if ((children as IJSExpression).type === 'JSExpression') {
|
||||
const handler = handlers.expression || handlers.common || noop;
|
||||
return handler(children as IJSExpression);
|
||||
} else {
|
||||
const handler = handlers.node || handlers.common || noop;
|
||||
return handler(children as IComponentNodeItem);
|
||||
}
|
||||
}
|
||||
28
packages/code-generator/src/utils/common.ts
Normal file
28
packages/code-generator/src/utils/common.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import changeCase from 'change-case';
|
||||
import short from 'short-uuid';
|
||||
|
||||
// Doc: https://www.npmjs.com/package/change-case
|
||||
|
||||
export function camel2dash(input: string): string {
|
||||
return changeCase.paramCase(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转为驼峰
|
||||
*/
|
||||
export function camelize(str: string): string {
|
||||
return changeCase.camelCase(str);
|
||||
}
|
||||
|
||||
export function generateID(): string {
|
||||
return short.generate();
|
||||
}
|
||||
|
||||
export function upperCaseFirst(inputValue: string): string {
|
||||
return changeCase.upperCaseFirst(inputValue);
|
||||
}
|
||||
|
||||
export function uniqueArray<T>(arr: T[]) {
|
||||
const uniqueItems = [...new Set<T>(arr)];
|
||||
return uniqueItems;
|
||||
}
|
||||
17
packages/code-generator/tsconfig.json
Normal file
17
packages/code-generator/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"outDir": "./lib",
|
||||
"lib": [
|
||||
"es6"
|
||||
],
|
||||
"types": ["node"],
|
||||
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { PluginProps } from '../../framework/definitions';
|
||||
const Save: React.FC<PluginProps> = (props): React.ReactElement => {
|
||||
const handleClick = (): void => {
|
||||
console.log('save data:', props.editor.designer.currentDocument.schema);
|
||||
console.log('save data json:', JSON.stringify(props.editor.designer.currentDocument.schema));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user