Merge branch 'feat/code-generator' into release/331

This commit is contained in:
春希 2020-03-27 13:54:02 +08:00
commit 93a4e82a91
71 changed files with 4183 additions and 1 deletions

View File

@ -1 +1,9 @@
出码模块
# 出码模块
## 安装接入
## 自定义导出
## 开始开发

View 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"
}

View 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';

View 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'];

View 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();

View 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;

View 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);
}
}

View 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),
);
}
}

View 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),
};
}

View 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,
});
}

View 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,
},
};

View 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');
}
}
}

View 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;
}
}

View 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;

View 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;

View 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;

View 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',
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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;

View File

@ -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;

View 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;

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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];
}

View 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;

View 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;

View 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;

View 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];
}

View 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'
);
}

View 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,
};
};

View 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();
});
});
};

View 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],
},
});
}

View 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>;
}

View 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; // 导入后名称
}

View 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);
}
}

View File

@ -0,0 +1,6 @@
export * from './core';
export * from './deps';
export * from './error';
export * from './result';
export * from './schema';
export * from './intermediate';

View 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
*/

View 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;
}

View 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: 单个请求结果
* dataschemaToCode中通过调用this.setState(...)state中
* Promiseresolve()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
}

View 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);
}
}

View 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;
}

View 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/**/*"
]
}

View File

@ -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 (