feat: 开源出码模块

This commit is contained in:
牧毅 2021-12-20 16:50:32 +08:00
parent 3ef5b7ff41
commit 1cd6561497
874 changed files with 44434 additions and 0 deletions

View File

@ -0,0 +1,13 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
quote_type = single
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,16 @@
# 忽略目录
node_modules/
build/
dist/
test-cases/
test/
tests/
output/
es/
lib/
coverage/
# 忽略文件
**/*.min.js
**/*-min.js
**/*.bundle.js

View File

@ -0,0 +1,8 @@
module.exports = {
extends: 'eslint-config-ali/typescript/react',
rules: {
'max-len': ['error', { code: 200 }],
'function-paren-newline': 'off',
'@typescript-eslint/indent': 'off',
},
};

110
packages/code-generator/.gitignore vendored Normal file
View File

@ -0,0 +1,110 @@
# project custom
build
es
dist
output
package-lock.json
yarn.lock
deploy-space/packages
deploy-space/.env
/demo/
/demo-output*/
/generated/
# IDE
.vscode
.idea
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
lib
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# mac config files
.DS_Store
# codealike
codealike.json
.node
# def publish
.package

View File

@ -0,0 +1,7 @@
module.exports = {
printWidth: 100,
tabWidth: 2,
semi: true,
singleQuote: true,
trailingComma: 'all',
};

View File

@ -0,0 +1,39 @@
# 如何共建
1. 拉取最新代码,切换到 develop 分支,基于 develop 分支切出一个 feature 或 hotfix 分支
2. 安装依赖(`yarn`),然后先跑一遍 `yarn test` 看看是否所有用例都能通过
3. 在 tests 目录下编写您的需求/问题的测试用例
4. 修改 src 下的一些代码,然后运行 `yarn test``yarn start` 启动 jest 进行调测
5. 确保所有的测试用例都能通过时,提 MR 给 @牧毅 -- MR 将在 1 个工作日内给您回复意见。
当然,欢迎提前私聊沟通 @牧毅,或加入 低代码渲染/出码服务金牌用户群 讨论沟通。
# FAQ
## 如何查看单测覆盖率?
执行 `yarn test:cov` 命令,这样会自动生成单测覆盖率的报告到 `coverage` 目录下。
## 如何只执行一个测试用例?
```sh
yarn test -t 'demo2-utils-name-alias'
```
## 更新特定测试用例的 expected:
```sh
yarn test:update-snapshots -t 'demo2-utils-name-alias'
```
## 如何只执行某个测试用例文件?
执行 `npx jest 测试用例的文件路径` 即可,如:
```sh
npx jest tests/plugins/common/requireUtils.test.ts
```
## 如何调试某个测试用例?
建议需要打断点的地方通过 VSCode 打上断点,然后打开 VSCode 的 JavaScript Debug Terminal在其中执行 `npx jest tests/path/to/your/test/file.ts``npx jest -t your-test-case-title` 来执行你的测试用例 -- 这样执行到打了断点的语句时会自动断住,以便调试。

View File

@ -0,0 +1,21 @@
# 出码模块
**重要!!! 本模块是 Node 端运行的!本模块是 Node 端运行的!本模块是 Node 端运行的!暂不支持 Web 端直接跑。**
如果有业务诉求需要在 Web 端运行,可以联系 @牧毅,会在架构组讨论优先度。
## 使用方法
需要快速体验效果的,可以:
- 使用命令行工具快速体验:`tnpx @ali/lowcode-code-generator -i example-schema.json -o generated -s icejs` (其中 example-schema.json 可以从[这里下载](https://unpkg.antfin-inc.com/@ali/lowcode-code-generator-cli@1.1.0/example-schema.json))
- 或访问 Git 仓库 [ali-lowcode/code-generator-demo](https://code.aone.alibaba-inc.com/ali-lowcode/code-generator-demo) 获取 demo 工程
更多更灵活地接入定制请参阅:
- [接入 & 定制出码](https://yuque.antfin.com/docs/share/5aab0684-3492-40bd-8bf2-53ae6ac1033d?#)
- [出码原理](https://yuque.antfin.com/docs/share/1a2f0415-a8ac-4159-b2e9-4cfebb5eab28?#)
## 参与共建
欢迎参与共建,如何共建请参阅:[./CONTRIBUTING.md](https://code.aone.alibaba-inc.com/ali-lowcode/code-generator/blob/develop/CONTRIBUTING.md)

View File

@ -0,0 +1,13 @@
{
"assets": {
"type": "command",
"command": {
"cmd": [
"tnpm install",
"node scripts/fixDefVersion ./package.json",
"tnpm run build",
"node scripts/move-files-to-build-dest"
]
}
}
}

View File

@ -0,0 +1,26 @@
#!/usr/bin/env node
/* eslint-disable no-var */
/* eslint-disable @typescript-eslint/no-require-imports */
var program = require('commander');
program
.description('Generate code from ali lowcode schema')
.requiredOption('-s, --solution <solution>', 'specify the solution to use (icejs/rax/recore)')
.option('-i, --input <input>', 'specify the input schema file')
.option('-o, --output <output>', 'specify the output directory', 'generated')
.option('-c, --cwd <cwd>', 'specify the working directory', '.')
.option('-q, --quiet', 'be quiet, do not output anything unless get error', false)
.arguments('ali lowcode schema JSON file')
.parse(process.argv);
var options = program.opts();
if (options.cwd) {
process.chdir(options.cwd);
}
require('../lib/cli')
.run(program.args, options)
.then((retCode) => {
process.exit(retCode);
});

View File

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

View File

@ -0,0 +1,276 @@
{
"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"
},
"lifeCycles": {
"componentDidMount": {
"type": "JSExpression",
"value": "function() { console.log('componentDidMount'); }"
}
},
"dataSource": {
"list": [
{
"id": "urlParams",
"type": "urlParams"
},
{
"id": "user",
"type": "fetch",
"options": {
"method": "GET",
"uri": "https://shs.alibaba-inc.com/mock/1458/demo/user",
"isSync": true
},
"dataHandler": {
"type": "JSExpression",
"value": "function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data;\n}"
}
},
{
"id": "orders",
"type": "fetch",
"options": {
"method": "GET",
"uri": "https://shs.alibaba-inc.com/mock/1458/demo/orders",
"isSync": true
},
"dataHandler": {
"type": "JSExpression",
"value": "function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data.result;\n}"
}
}
],
"dataHandler": {
"type": "JSExpression",
"value": "function (dataMap) {\n console.info(\"All datasources loaded:\", dataMap);\n}"
}
},
"children": [
{
"componentName": "Form",
"id": "node$2",
"props": {
"labelCol": {
"type": "JSExpression",
"value": "this.state.colNum"
},
"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",
"condition": {
"type": "JSExpression",
"value": "this.index >= 1"
},
"loop": ["a", "b", "c"],
"props": {
"type": "primary",
"style": {
"margin": "0 5px 0 5px"
}
},
"children": [
{
"type": "JSExpression",
"value": "this.item"
}
]
}
]
}
]
}
]
}
]
}
],
"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": "Test"
}
}

View File

@ -0,0 +1,9 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transformIgnorePatterns: ['/node_modules/(?!core-js)/'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverage: false,
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!**/node_modules/**', '!**/vendor/**'],
testMatch: ['<rootDir>/tests/**/*.test.ts'],
};

View File

@ -0,0 +1,128 @@
{
"name": "@ali/lowcode-code-generator",
"version": "1.3.0",
"description": "出码引擎 for LowCode Engine",
"license": "MIT",
"main": "lib/index.js",
"module": "es/index.js",
"typings": "es/index.d.ts",
"files": [
"bin",
"lib",
"es",
"demo",
"dist",
"CHANGELOG.md",
"README.md",
"example-schema.json"
],
"bin": {
"lowcode-code-generator": "bin/lowcode-code-generator.js"
},
"scripts": {
"start": "jest --watchAll",
"build": "npm run build:bs",
"build:bs": "rimraf lib es dist && npm run build:cjs && npm run build:esm",
"build:cjs": "tsc --module commonjs --outDir lib",
"build:esm": "tsc --module es6 --outDir es",
"clean": "rimraf es lib dist test-cases/*/*/actual",
"lint": "eslint --ext .jsx,.js,.ts,.tsx src/",
"lintfix": "eslint --ext .jsx,.js,.ts,.tsx --fix src/",
"template": "node ./scripts/build-template-static-files.js",
"test": "jest",
"test:cov": "jest --coverage",
"test:update-snapshots": "cross-env UPDATE_EXPECTED=true jest -u",
"release:beta": "standard-version -t @ali/lowcode-code-generator\\@ --prerelease beta && git push --follow-tags && tnpm publish --tag beta",
"release": "standard-version -t @ali/lowcode-code-generator\\@ && git push --follow-tags && tnpm publish",
"prepublishOnly": "npm run build",
"demo": "node bin/lowcode-code-generator.js -i example-schema.json -o demo -s icejs"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": "eslint"
},
"dependencies": {
"@ali/lowcode-types": "^1.0.50",
"@babel/generator": "^7.12.11",
"@babel/parser": "^7.12.11",
"@babel/runtime": "^7.12.5",
"@babel/traverse": "^7.12.12",
"@babel/types": "^7.12.12",
"@types/debug": "^4.1.7",
"@types/fs-extra": "^9.0.12",
"@types/glob": "^7.2.0",
"@types/lodash": "^4.14.162",
"@types/qs": "^6.9.6",
"@types/semver": "^7.3.4",
"chalk": "^4.1.0",
"change-case": "^3.1.0",
"commander": "^6.1.0",
"debug": "^4.3.2",
"fs-extra": "9.x",
"glob": "^7.2.0",
"html-entities": "^2.3.2",
"json5": "^2.2.0",
"jsonc": "^2.0.0",
"jszip": "^3.5.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"mock-fs": "^5.1.2",
"moment": "^2.29.1",
"path-browserify": "^1.0.1",
"prettier": "^2.5.1",
"qs": "^6.10.1",
"semver": "^7.3.4",
"short-uuid": "^3.1.1",
"tslib": "^2.3.1"
},
"browser": {
"path": "path-browserify",
"lodash": "lodash-es",
"prettier": "prettier/standalone"
},
"devDependencies": {
"@iceworks/spec": "^1.4.2",
"@types/babel__traverse": "^7.11.0",
"@types/jest": "^27.0.2",
"@types/lodash": "^4.14.162",
"@types/node": "^14.14.20",
"@types/prettier": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"build-plugin-component": "^0.2.22",
"cross-env": "^7.0.3",
"esbuild": "^0.14.5",
"esbuild-plugin-ignore": "^1.1.0",
"esbuild-visualizer": "^0.3.1",
"eslint": "^7.17.0",
"eslint-config-ali": "^11.4.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"jest": "^27.3.1",
"rimraf": "^3.0.2",
"standard-version": "^9.1.1",
"ts-jest": "^27.0.7",
"ts-loader": "^6.2.2",
"ts-node": "^8.10.2",
"tsconfig-paths": "^3.9.0",
"typescript": "4.x",
"yargs-parser": "^20.2.9"
},
"engines": {
"node": ">=10.0.0",
"install-node": "14.x"
},
"publishConfig": {
"registry": "http://registry.npm.alibaba-inc.com"
},
"tnpm": {
"mode": "yarn",
"lockfile": "enable"
}
}

View File

@ -0,0 +1,125 @@
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/quotes */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-require-imports */
// @ts-check
// 这个文件是用来构建模板中的静态文件的
const fs = require('fs');
const glob = require('glob');
const path = require('path');
const JSON5 = require('json5');
const { spawnSync } = require('child_process');
const PROJECT_ROOT = path.join(__dirname, '..');
const TEMPLATES = [
{
sourceDir: path.join(PROJECT_ROOT, 'static-files/rax'),
outputDir: path.join(PROJECT_ROOT, 'src/plugins/project/framework/rax/template'),
},
];
try {
TEMPLATES.forEach(buildTemplateStaticFiles);
console.log('All done.');
} catch (e) {
console.error(e);
process.exit(1);
}
function buildTemplateStaticFiles({ sourceDir, outputDir }) {
console.log('processing %s template...', path.dirname(sourceDir));
// 扫描所有的目录
const sourceFiles = glob.sync('**/*', {
nodir: true,
dot: true,
cwd: sourceDir,
});
console.log('got %d files: %o', sourceFiles.length, sourceFiles);
const staticFiles = {
imports: [],
runs: [],
};
// 生成对应的文件
sourceFiles.forEach((sourceFileName, index) => {
console.log('processing %s', sourceFileName);
const sourceFileContent = fs.readFileSync(path.join(sourceDir, sourceFileName), 'utf-8');
const sourceFileRealName = sourceFileName.replace(/\.template$/, '');
const outputFileName = `${sourceFileRealName}.ts`;
const outputFileFullPath = path.join(outputDir, 'files', outputFileName);
const sourceFileExtName = path.extname(sourceFileRealName);
const sourceFileBaseName = path.basename(sourceFileRealName, sourceFileExtName);
// 确保目录存在
fs.mkdirSync(path.dirname(outputFileFullPath), { recursive: true });
// 写入文件
fs.writeFileSync(
outputFileFullPath,
[
`/* eslint-disable max-len */`,
`/* Note: this file is generated by "npm run template", please dont modify this file directly */`,
`/* -- instead, you should modify "${path.relative(
PROJECT_ROOT,
path.join(sourceDir, sourceFileName),
)}" and run "npm run template" */`,
`import { ResultFile } from '@ali/lowcode-types';`,
'',
`export default function getFile(): [string[], ResultFile] {`,
` return ${JSON5.stringify([
// 文件目录:
path.dirname(sourceFileRealName).split(path.sep).filter(Boolean),
// 文件名和内容:
{
name: sourceFileBaseName,
ext: sourceFileExtName.replace(/^\./, ''),
content: sourceFileContent,
},
])};`,
`}`,
'',
].join('\n'),
{
encoding: 'utf-8',
},
);
staticFiles.imports.push(`import file${index} from './files/${sourceFileRealName}';`);
staticFiles.runs.push(` runFileGenerator(root, file${index})`);
});
console.log('generating static-files.ts...');
fs.writeFileSync(
path.join(outputDir, 'static-files.ts'),
[
`/* Note: this file is generated by "npm run template", please dont modify this file directly */`,
`import { ResultDir } from '@ali/lowcode-types';
import { createResultDir } from '../../../../../utils/resultHelper';
import { runFileGenerator } from '../../../../../utils/templateHelper';`,
...staticFiles.imports,
'',
`export function generateStaticFiles(root = createResultDir('.')): ResultDir {`,
...staticFiles.runs,
` return root;`,
`}`,
'',
].join('\n'),
{ encoding: 'utf-8' },
);
// prettier 一把
console.log('run prettier...');
spawnSync('npx', ['prettier', '--write', `${outputDir}`], {
stdio: 'inherit',
shell: true,
});
console.log('done %s', path.basename(sourceDir));
}

View File

@ -0,0 +1,80 @@
#!/usr/bin/env node
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-require-imports */
// @ts-check - check the types to avoid silly mistakes
// This is a script to fix the version in package.json during DEF publishing.
// Test this file:
//
// $ BUILD_GIT_BRANCH=release/1.1.3 BUILD_ARGV_STR=--def_publish_env=daily node scripts/fixDefVersion ./package.json
// --> should fix the package.json version to 1.1.3-beta.xxxx
//
// $ BUILD_GIT_BRANCH=release/1.1.3 BUILD_ARGV_STR=--def_publish_env=prod node scripts/fixDefVersion ./package.json
// --> should fix the package.json version to 1.1.3
const fs = require('fs');
const moment = require('moment');
const program = require('commander');
const parseArgs = require('yargs-parser');
program
.description('Fix version for def publishing TNPM packages')
.option('--no-beta', 'no beta version', false)
.arguments('package.json file path (only one is needed)')
.parse(process.argv);
try {
const packageJsonFilePath = program.args[0];
if (!packageJsonFilePath) {
program.help();
process.exit(2);
}
const destVersion = fixVersion({
packageJsonFilePath,
env: process.env,
beta: program.opts().beta,
});
console.log(`Fixed version to: ${destVersion}`);
} catch (err) {
console.error('Got error: ', err);
process.exit(1);
}
function fixVersion({ packageJsonFilePath, env = process.env, beta = true }) {
if (!env.BUILD_GIT_BRANCH) {
throw new Error('env.BUILD_GIT_BRANCH is required');
}
if (!env.BUILD_ARGV_STR) {
throw new Error('env.BUILD_ARGV_STR is required');
}
const gitBranchVersion = parseBuildBranchVersion(env.BUILD_GIT_BRANCH);
const buildArgs = parseArgs(env.BUILD_ARGV_STR);
const buildEnv = buildArgs.def_publish_env; // daily | prod
const destVersion =
buildEnv === 'prod' || !beta
? gitBranchVersion
: `${gitBranchVersion}-beta.${moment().format('MMDDHHmm').replace(/^0+/, '')}`;
const packageJson = JSON.parse(fs.readFileSync(packageJsonFilePath, 'utf-8'));
packageJson.version = destVersion;
if (env.BUILD_GIT_COMMITID) {
packageJson.gitHead = env.BUILD_GIT_COMMITID;
}
fs.writeFileSync(packageJsonFilePath, `${JSON.stringify(packageJson, null, 2)}\n`, {
encoding: 'utf8',
});
return destVersion;
}
function parseBuildBranchVersion(branchName) {
const m = `${branchName}`.match(/\d+\.\d+\.\d+/);
return (m && m[0]) || '';
}

View File

@ -0,0 +1,24 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const fs = require('fs');
const { spawnSync } = require('child_process');
const BUILD_DEST = process.env.BUILD_DEST || '.package';
fs.mkdirSync(BUILD_DEST, { recursive: true });
const distFiles = [...require('../package.json').files, 'package.json'];
distFiles.forEach((file) => {
console.log('mv %s', file);
if (file === BUILD_DEST) {
fs.mkdirSync(`${BUILD_DEST}/${file}`, { recursive: true });
spawnSync('mv', [`${file}/*`, `${BUILD_DEST}/${file}/`], { shell: true, stdio: 'inherit' });
}
});
distFiles.forEach((file) => {
console.log('mv %s', file);
if (file !== BUILD_DEST) {
spawnSync('mv', [file, `${BUILD_DEST}/${file}`], { shell: true, stdio: 'inherit' });
}
});

View File

@ -0,0 +1,35 @@
import type { NodeSchema, CompositeObject } from '@ali/lowcode-types';
import type { TComponentAnalyzer } from '../types';
import { handleSubNodes } from '../utils/schema';
export const componentAnalyzer: TComponentAnalyzer = (container) => {
let hasRefAttr = false;
const nodeValidator = (n: NodeSchema) => {
if (n.props) {
const props = n.props as CompositeObject;
if (props.ref) {
hasRefAttr = true;
}
}
};
nodeValidator(container);
if (!hasRefAttr && container.children) {
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
handleSubNodes<void>(
container.children,
{
node: nodeValidator,
},
{
rerun: true,
},
);
}
return {
isUsingRef: hasRefAttr,
};
};

View File

@ -0,0 +1 @@
export * from './run';

View File

@ -0,0 +1,129 @@
/* eslint-disable no-console */
import chalk from 'chalk';
import * as fs from 'fs-extra';
import JSON5 from 'json5';
import { jsonc } from 'jsonc';
import { spawnSync } from 'child_process';
import * as path from 'path';
import { getErrorMessage } from '../utils/errors';
import CodeGenerator from '..';
import type { IProjectBuilder } from '..';
import type { ProjectSchema } from '@ali/lowcode-types';
/**
* CLI
* @param args
* @param options
* @returns {Promise<number>}
*/
export async function run(
args: string[],
options: {
solution: string;
input?: string;
output?: string;
quiet?: boolean;
},
): Promise<number> {
try {
const schemaFile = options.input || args[0];
if (!schemaFile) {
throw new Error(
'a schema file must be specified by `--input <schema.json>` or by the first positional argument',
);
}
if ((options.input && args.length > 0) || args.length > 1) {
throw new Error(
'only one schema file can be specified, either by `--input <schema.json>` or by the first positional argument',
);
}
// 读取 Schema
const schema = await loadSchemaFile(schemaFile);
// 创建一个项目构建器
const createProjectBuilder = await getProjectBuilderFactory(options.solution, {
quiet: options.quiet,
});
const builder = createProjectBuilder();
// 生成代码
const generatedSourceCodes = await builder.generateProject(schema);
// 输出到磁盘
const publisher = CodeGenerator.publishers.disk();
await publisher.publish({
project: generatedSourceCodes,
outputPath: options.output || 'generated',
projectSlug: 'example',
createProjectFolder: false,
});
return 0;
} catch (e) {
console.log(chalk.red(getErrorMessage(e) || `Unexpected error: ${e}`));
return 1;
}
}
async function getProjectBuilderFactory(
solution: string,
{ quiet }: { quiet?: boolean },
): Promise<() => IProjectBuilder> {
if (solution in CodeGenerator.solutions) {
return CodeGenerator.solutions[solution as 'icejs' | 'rax'];
}
const solutionPackageName = isLocalSolution(solution)
? solution
: `${solution.startsWith('@') ? solution : `@ali/lowcode-solution-${solution}`}`;
if (!isLocalSolution(solution)) {
if (!quiet) {
console.log(`"${solution}" is not internal, installing it as ${solutionPackageName}...`);
}
spawnSync('tnpm', ['i', solutionPackageName], {
stdio: quiet ? 'ignore' : 'inherit',
});
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
const solutionExports = require(!isLocalSolution(solution)
? solutionPackageName
: `${path.isAbsolute(solution) ? solution : path.join(process.cwd(), solution)}`);
const projectBuilderFactory =
solutionExports.createProjectBuilder ||
solutionExports.createAppBuilder ||
solutionExports.default;
if (typeof projectBuilderFactory !== 'function') {
throw new Error(
`"${solutionPackageName}" should export project builder factory via named export 'createProjectBuilder' or via default export`,
);
}
return projectBuilderFactory;
}
function isLocalSolution(solution: string) {
return solution.startsWith('.') || solution.startsWith('/') || solution.startsWith('~');
}
async function loadSchemaFile(schemaFile: string): Promise<ProjectSchema> {
if (!schemaFile) {
throw new Error('invalid schema file name');
}
const schemaFileContent = await fs.readFile(schemaFile, 'utf8');
if (/\.json5/.test(schemaFile)) {
return JSON5.parse(schemaFileContent);
}
// 默认用 JSONC 的格式解析(兼容 JSON
return jsonc.parse(schemaFileContent);
}

View File

@ -0,0 +1,3 @@
import * as path from 'path';
export const CODE_GENERATOR_ROOT = path.join(__dirname, '../..');

View File

@ -0,0 +1,3 @@
import { FileType } from '../types/core';
export const FILE_TYPE_FAMILY = [[FileType.TSX, FileType.TS, FileType.JSX, FileType.JS]];

View File

@ -0,0 +1,129 @@
export const COMMON_CHUNK_NAME = {
ExternalDepsImport: 'CommonExternalDependencyImport',
InternalDepsImport: 'CommonInternalDependencyImport',
ImportAliasDefine: 'CommonImportAliasDefine',
FileVarDefine: 'CommonFileScopeVarDefine',
FileUtilDefine: 'CommonFileScopeMethodDefine',
FileMainContent: 'CommonFileMainContent',
FileExport: 'CommonFileExport',
StyleDepsImport: 'CommonStyleDepsImport',
StyleCssContent: 'CommonStyleCssContent',
HtmlContent: 'CommonHtmlContent',
CustomContent: 'CommonCustomContent',
};
export const CLASS_DEFINE_CHUNK_NAME = {
Start: 'CommonClassDefineStart',
ConstructorStart: 'CommonClassDefineConstructorStart',
ConstructorContent: 'CommonClassDefineConstructorContent',
ConstructorEnd: 'CommonClassDefineConstructorEnd',
StaticVar: 'CommonClassDefineStaticVar',
StaticMethod: 'CommonClassDefineStaticMethod',
InsVar: 'CommonClassDefineInsVar',
InsVarMethod: 'CommonClassDefineInsVarMethod',
InsMethod: 'CommonClassDefineInsMethod',
InsPrivateMethod: 'CommonClassDefineInsPrivateMethod',
End: 'CommonClassDefineEnd',
};
export const DEFAULT_LINK_AFTER = {
[COMMON_CHUNK_NAME.ExternalDepsImport]: [],
[COMMON_CHUNK_NAME.InternalDepsImport]: [COMMON_CHUNK_NAME.ExternalDepsImport],
[COMMON_CHUNK_NAME.ImportAliasDefine]: [
COMMON_CHUNK_NAME.ExternalDepsImport, COMMON_CHUNK_NAME.InternalDepsImport,
],
[COMMON_CHUNK_NAME.FileVarDefine]: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
],
[COMMON_CHUNK_NAME.FileUtilDefine]: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
],
[CLASS_DEFINE_CHUNK_NAME.Start]: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
COMMON_CHUNK_NAME.FileUtilDefine,
],
[CLASS_DEFINE_CHUNK_NAME.ConstructorStart]: [
CLASS_DEFINE_CHUNK_NAME.Start,
CLASS_DEFINE_CHUNK_NAME.StaticVar,
CLASS_DEFINE_CHUNK_NAME.StaticMethod,
CLASS_DEFINE_CHUNK_NAME.InsVar,
CLASS_DEFINE_CHUNK_NAME.InsVarMethod,
],
[CLASS_DEFINE_CHUNK_NAME.ConstructorContent]: [CLASS_DEFINE_CHUNK_NAME.ConstructorStart],
[CLASS_DEFINE_CHUNK_NAME.ConstructorEnd]: [
CLASS_DEFINE_CHUNK_NAME.ConstructorStart,
CLASS_DEFINE_CHUNK_NAME.ConstructorContent,
],
[CLASS_DEFINE_CHUNK_NAME.StaticVar]: [CLASS_DEFINE_CHUNK_NAME.Start],
[CLASS_DEFINE_CHUNK_NAME.StaticMethod]: [
CLASS_DEFINE_CHUNK_NAME.Start, CLASS_DEFINE_CHUNK_NAME.StaticVar,
],
[CLASS_DEFINE_CHUNK_NAME.InsVar]: [
CLASS_DEFINE_CHUNK_NAME.Start,
CLASS_DEFINE_CHUNK_NAME.StaticVar,
CLASS_DEFINE_CHUNK_NAME.StaticMethod,
],
[CLASS_DEFINE_CHUNK_NAME.InsVarMethod]: [
CLASS_DEFINE_CHUNK_NAME.Start,
CLASS_DEFINE_CHUNK_NAME.StaticVar,
CLASS_DEFINE_CHUNK_NAME.StaticMethod,
CLASS_DEFINE_CHUNK_NAME.InsVar,
],
[CLASS_DEFINE_CHUNK_NAME.InsMethod]: [
CLASS_DEFINE_CHUNK_NAME.Start,
CLASS_DEFINE_CHUNK_NAME.StaticVar,
CLASS_DEFINE_CHUNK_NAME.StaticMethod,
CLASS_DEFINE_CHUNK_NAME.InsVar,
CLASS_DEFINE_CHUNK_NAME.InsVarMethod,
CLASS_DEFINE_CHUNK_NAME.ConstructorEnd,
],
[CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod]: [
CLASS_DEFINE_CHUNK_NAME.Start,
CLASS_DEFINE_CHUNK_NAME.StaticVar,
CLASS_DEFINE_CHUNK_NAME.StaticMethod,
CLASS_DEFINE_CHUNK_NAME.InsVar,
CLASS_DEFINE_CHUNK_NAME.InsVarMethod,
CLASS_DEFINE_CHUNK_NAME.InsMethod,
CLASS_DEFINE_CHUNK_NAME.ConstructorEnd,
],
[CLASS_DEFINE_CHUNK_NAME.End]: [
CLASS_DEFINE_CHUNK_NAME.Start,
CLASS_DEFINE_CHUNK_NAME.StaticVar,
CLASS_DEFINE_CHUNK_NAME.StaticMethod,
CLASS_DEFINE_CHUNK_NAME.InsVar,
CLASS_DEFINE_CHUNK_NAME.InsVarMethod,
CLASS_DEFINE_CHUNK_NAME.InsMethod,
CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod,
CLASS_DEFINE_CHUNK_NAME.ConstructorEnd,
],
[COMMON_CHUNK_NAME.FileMainContent]: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
COMMON_CHUNK_NAME.FileUtilDefine,
CLASS_DEFINE_CHUNK_NAME.End,
],
[COMMON_CHUNK_NAME.FileExport]: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
COMMON_CHUNK_NAME.FileUtilDefine,
CLASS_DEFINE_CHUNK_NAME.End,
COMMON_CHUNK_NAME.FileMainContent,
],
[COMMON_CHUNK_NAME.StyleDepsImport]: [],
[COMMON_CHUNK_NAME.StyleCssContent]: [COMMON_CHUNK_NAME.StyleDepsImport],
[COMMON_CHUNK_NAME.HtmlContent]: [],
};
export const COMMON_SUB_MODULE_NAME = 'index';

View File

@ -0,0 +1,12 @@
export const NATIVE_ELE_PKG = 'native';
export const CONTAINER_TYPE = {
COMPONENT: 'Component',
BLOCK: 'Block',
PAGE: 'Page',
};
export const SUPPORT_SCHEMA_VERSION_LIST = ['0.0.1', '1.0.0'];
export * from './file';
export * from './generator';

View File

@ -0,0 +1,123 @@
import { BuilderComponentPlugin, IChunkBuilder, ICodeChunk, ICodeStruct, FileType } from '../types';
import { COMMON_SUB_MODULE_NAME } from '../const/generator';
import { FILE_TYPE_FAMILY } from '../const/file';
interface ChunkGroupInfo {
chunk: ICodeChunk;
familyIdx?: number;
}
function whichFamily(type: FileType): [number, FileType[]] | undefined {
const idx = FILE_TYPE_FAMILY.findIndex((family) => family.indexOf(type) >= 0);
if (idx < 0) {
return undefined;
}
return [idx, FILE_TYPE_FAMILY[idx]];
}
export const groupChunks = (chunks: ICodeChunk[]): ICodeChunk[][] => {
const tmp: Record<string, Record<number, number>> = {};
const col = chunks.reduce((chunksSet: Record<string, ChunkGroupInfo[]>, chunk) => {
const fileKey = chunk.subModule || COMMON_SUB_MODULE_NAME;
if (!chunksSet[fileKey]) {
// eslint-disable-next-line no-param-reassign
chunksSet[fileKey] = [];
}
const res = whichFamily(chunk.fileType as FileType);
const info: ChunkGroupInfo = {
chunk,
};
if (res) {
const [familyIdx, family] = res;
const rank = family.indexOf(chunk.fileType as FileType);
if (tmp[fileKey]) {
if (tmp[fileKey][familyIdx] !== undefined) {
if (tmp[fileKey][familyIdx] > rank) {
tmp[fileKey][familyIdx] = rank;
}
} else {
tmp[fileKey][familyIdx] = rank;
}
} else {
tmp[fileKey] = {};
tmp[fileKey][familyIdx] = rank;
}
info.familyIdx = familyIdx;
}
chunksSet[fileKey].push(info);
return chunksSet;
}, {});
const result: ICodeChunk[][] = [];
Object.keys(col).forEach((key) => {
const byType: Record<string, ICodeChunk[]> = {};
col[key].forEach((info) => {
let t: string = info.chunk.fileType;
if (info.familyIdx !== undefined) {
t = FILE_TYPE_FAMILY[info.familyIdx][tmp[key][info.familyIdx]];
// eslint-disable-next-line no-param-reassign
info.chunk.fileType = t;
}
if (!byType[t]) {
byType[t] = [];
}
byType[t].push(info.chunk);
});
result.push(...Object.keys(byType).map((t) => byType[t]));
});
return result;
};
/**
*
*
* @export
* @class ChunkBuilder
* @template T
*/
export class ChunkBuilder implements IChunkBuilder {
private plugins: BuilderComponentPlugin[];
constructor(plugins: BuilderComponentPlugin[] = []) {
this.plugins = plugins;
}
async run(
ir: unknown,
initialStructure: ICodeStruct = {
ir,
chunks: [],
depNames: [],
contextData: {},
},
) {
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,
};
}
getPlugins() {
return this.plugins;
}
addPlugin(plugin: BuilderComponentPlugin) {
this.plugins.push(plugin);
}
}
export default ChunkBuilder;

View File

@ -0,0 +1,103 @@
import {
ChunkContent,
ChunkType,
CodeGeneratorError,
CodeGeneratorFunction,
ICodeBuilder,
ICodeChunk,
} from '../types';
export class CodeBuilder implements ICodeBuilder {
private chunkDefinitions: ICodeChunk[] = [];
private generators: { [key: string]: CodeGeneratorFunction<ChunkContent> } = {
[ChunkType.STRING]: (str: string) => str, // no-op for string chunks
[ChunkType.JSON]: (json: Record<string, unknown>) => 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.
*/
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);
if (!unprocessedChunks.some((ch) => ch.name === name)) {
unprocessedChunks.forEach(
// remove the processed chunk from all the linkAfter arrays from the remaining chunks
(ch) => {
// eslint-disable-next-line no-param-reassign
ch.linkAfter = ch.linkAfter.filter((after) => after !== name);
},
);
}
}
return resultingString.join('\n');
}
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));
}
}
export default CodeBuilder;

View File

@ -0,0 +1,109 @@
import { ProjectSchema, ResultFile, ResultDir } from '@ali/lowcode-types';
import {
BuilderComponentPlugin,
CodeGeneratorError,
ICodeChunk,
ICompiledModule,
IModuleBuilder,
IParseResult,
ISchemaParser,
PostProcessor,
} from '../types';
import { COMMON_SUB_MODULE_NAME } from '../const/generator';
import { SchemaParser } from '../parser/SchemaParser';
import { ChunkBuilder } from './ChunkBuilder';
import { CodeBuilder } from './CodeBuilder';
import { createResultFile, createResultDir, addFile } from '../utils/resultHelper';
export function createModuleBuilder(
options: {
plugins: BuilderComponentPlugin[];
postProcessors: PostProcessor[];
mainFileName?: string;
} = {
plugins: [],
postProcessors: [],
},
): 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!',
);
}
let files: ResultFile[] = [];
const { chunks } = await chunkGenerator.run(input);
chunks.forEach((fileChunkList) => {
const content = linker.link(fileChunkList);
const file = createResultFile(
fileChunkList[0].subModule || moduleMainName,
fileChunkList[0].fileType,
content,
);
files.push(file);
});
if (options.postProcessors.length > 0) {
files = files.map((file) => {
let { content } = file;
const type = file.ext;
options.postProcessors.forEach((processer) => {
content = processer(content, type);
});
return createResultFile(file.name, type, content);
});
}
return {
files,
};
};
const generateModuleCode = async (schema: ProjectSchema | string): Promise<ResultDir> => {
// Init
const schemaParser: ISchemaParser = new SchemaParser();
const parseResult: IParseResult = schemaParser.parse(schema);
const containerInfo = parseResult.containers[0];
const { files } = await generateModule(containerInfo);
const dir = createResultDir(containerInfo.moduleName);
files.forEach((file) => addFile(dir, file));
return dir;
};
const linkCodeChunks = (chunks: Record<string, ICodeChunk[]>, fileName: string) => {
const files: ResultFile[] = [];
Object.keys(chunks).forEach((fileKey) => {
const fileChunkList = chunks[fileKey];
const content = linker.link(fileChunkList);
const file = createResultFile(
fileChunkList[0].subModule || fileName,
fileChunkList[0].fileType,
content,
);
files.push(file);
});
return files;
};
return {
generateModule,
generateModuleCode,
linkCodeChunks,
addPlugin: chunkGenerator.addPlugin.bind(chunkGenerator),
};
}

View File

@ -0,0 +1,294 @@
import { ResultDir, ResultFile, ProjectSchema } from '@ali/lowcode-types';
import {
IModuleBuilder,
IParseResult,
IProjectBuilder,
IProjectPlugins,
IProjectTemplate,
ISchemaParser,
PostProcessor,
} from '../types';
import { SchemaParser } from '../parser/SchemaParser';
import { createResultDir, addDirectory, addFile } from '../utils/resultHelper';
import { createModuleBuilder } from '../generator/ModuleBuilder';
import { ProjectPreProcessor, ProjectPostProcessor } from '../types/core';
import { CodeGeneratorError } from '../types/error';
interface IModuleInfo {
moduleName?: string;
path: string[];
files: ResultFile[];
}
export interface ProjectBuilderInitOptions {
/** 项目模板 */
template: IProjectTemplate;
/** 项目插件 */
plugins: IProjectPlugins;
/** 模块后置处理器 */
postProcessors: PostProcessor[];
/** Schema 解析器 */
schemaParser?: ISchemaParser;
/** 项目级别的前置处理器 */
projectPreProcessors?: ProjectPreProcessor[];
/** 项目级别的后置处理器 */
projectPostProcessors?: ProjectPostProcessor[];
}
export class ProjectBuilder implements IProjectBuilder {
/** 项目模板 */
private template: IProjectTemplate;
/** 项目插件 */
private plugins: IProjectPlugins;
/** 模块后置处理器 */
private postProcessors: PostProcessor[];
/** Schema 解析器 */
private schemaParser: ISchemaParser;
/** 项目级别的前置处理器 */
private projectPreProcessors: ProjectPreProcessor[];
/** 项目级别的后置处理器 */
private projectPostProcessors: ProjectPostProcessor[];
constructor({
template,
plugins,
postProcessors,
schemaParser = new SchemaParser(),
projectPreProcessors = [],
projectPostProcessors = [],
}: ProjectBuilderInitOptions) {
this.template = template;
this.plugins = plugins;
this.postProcessors = postProcessors;
this.schemaParser = schemaParser;
this.projectPreProcessors = projectPreProcessors;
this.projectPostProcessors = projectPostProcessors;
}
async generateProject(originalSchema: ProjectSchema | string): Promise<ResultDir> {
// Init
const { schemaParser } = this;
const builders = this.createModuleBuilders();
const projectRoot = await this.template.generateTemplate();
let schema: ProjectSchema =
typeof originalSchema === 'string' ? JSON.parse(originalSchema) : originalSchema;
// Validate
if (!schemaParser.validate(schema)) {
throw new CodeGeneratorError('Schema is invalid');
}
// Parse / Format
// Preprocess
for (const preProcessor of this.projectPreProcessors) {
// eslint-disable-next-line no-await-in-loop
schema = await preProcessor(schema);
}
// Collect 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.moduleName,
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,
});
}
// appConfig
if (builders.appConfig) {
const { files } = await builders.appConfig.generateModule(parseResult);
buildResult.push({
path: this.template.slots.appConfig.path,
files,
});
}
// buildConfig
if (builders.buildConfig) {
const { files } = await builders.buildConfig.generateModule(parseResult);
buildResult.push({
path: this.template.slots.buildConfig.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 (builders.i18n && this.template.slots.i18n) {
const { files } = await builders.i18n.generateModule(parseResult.project);
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,
});
}
// TODO: 更多 slots 的处理??是不是可以考虑把 template 中所有的 slots 都处理下?
// Post Process
// Combine Modules
buildResult.forEach((moduleInfo) => {
let targetDir = getDirFromRoot(projectRoot, moduleInfo.path);
if (moduleInfo.moduleName) {
const dir = createResultDir(moduleInfo.moduleName);
addDirectory(targetDir, dir);
targetDir = dir;
}
moduleInfo.files.forEach((file) => addFile(targetDir, file));
});
// post-processors
let finalResult = projectRoot;
for (const projectPostProcessor of this.projectPostProcessors) {
// eslint-disable-next-line no-await-in-loop
finalResult = await projectPostProcessor(finalResult, schema, originalSchema);
}
return finalResult;
}
private createModuleBuilders(): Record<string, IModuleBuilder> {
const builders: Record<string, IModuleBuilder> = {};
Object.keys(this.plugins).forEach((pluginName) => {
if (this.plugins[pluginName].length > 0) {
const options: { mainFileName?: string } = {};
if (this.template.slots[pluginName] && this.template.slots[pluginName].fileName) {
options.mainFileName = this.template.slots[pluginName].fileName;
}
builders[pluginName] = createModuleBuilder({
plugins: this.plugins[pluginName],
postProcessors: this.postProcessors,
...options,
});
}
});
return builders;
}
}
export function createProjectBuilder(initOptions: ProjectBuilderInitOptions): IProjectBuilder {
return new ProjectBuilder(initOptions);
}
function getDirFromRoot(root: ResultDir, path: string[]): ResultDir {
let current: ResultDir = root;
path.forEach((p) => {
const exist = current.dirs.find((d) => d.name === p);
if (exist) {
current = exist;
} else {
const newDir = createResultDir(p);
addDirectory(current, newDir);
current = newDir;
}
});
return current;
}

View File

@ -0,0 +1,122 @@
/**
* Schema
* API , API
* export { xxx } from 'xx' export * from 'xxx')
* API tests/public
*/
import { createProjectBuilder } from './generator/ProjectBuilder';
import { createModuleBuilder } from './generator/ModuleBuilder';
import { createDiskPublisher } from './publisher/disk';
import { createZipPublisher } from './publisher/zip';
import createIceJsProjectBuilder, { plugins as reactPlugins } from './solutions/icejs';
import createRaxAppProjectBuilder, { plugins as raxPlugins } from './solutions/rax-app';
// 引入说明
import { REACT_CHUNK_NAME } from './plugins/component/react/const';
import { COMMON_CHUNK_NAME, CLASS_DEFINE_CHUNK_NAME, DEFAULT_LINK_AFTER } from './const/generator';
// 引入通用插件组
import esmodule from './plugins/common/esmodule';
import requireUtils from './plugins/common/requireUtils';
import css from './plugins/component/style/css';
import constants from './plugins/project/constants';
import i18n from './plugins/project/i18n';
import utils from './plugins/project/utils';
import prettier from './postprocessor/prettier';
// 引入常用工具
import * as utilsCommon from './utils/common';
import * as utilsCompositeType from './utils/compositeType';
import * as utilsJsExpression from './utils/jsExpression';
import * as utilsJsSlot from './utils/jsSlot';
import * as utilsNodeToJSX from './utils/nodeToJSX';
import * as utilsResultHelper from './utils/resultHelper';
import * as utilsTemplateHelper from './utils/templateHelper';
import * as utilsValidate from './utils/validate';
import * as utilsSchema from './utils/schema';
import * as CONSTANTS from './const';
// 引入内置解决方案模块
import icejs from './plugins/project/framework/icejs';
import rax from './plugins/project/framework/rax';
export default {
createProjectBuilder,
createModuleBuilder,
solutions: {
icejs: createIceJsProjectBuilder,
rax: createRaxAppProjectBuilder,
},
solutionParts: {
icejs,
rax,
},
publishers: {
disk: createDiskPublisher,
zip: createZipPublisher,
},
plugins: {
common: {
/**
* ES Module
* @deprecated please use esModule
*/
esmodule,
esModule: esmodule,
requireUtils,
},
react: {
...reactPlugins,
},
rax: {
...raxPlugins,
},
style: {
css,
},
project: {
constants,
i18n,
utils,
},
},
postprocessor: {
prettier,
},
utils: {
common: utilsCommon,
compositeType: utilsCompositeType,
jsExpression: utilsJsExpression,
jsSlot: utilsJsSlot,
nodeToJSX: utilsNodeToJSX,
resultHelper: utilsResultHelper,
templateHelper: utilsTemplateHelper,
validate: utilsValidate,
schema: utilsSchema,
},
chunkNames: {
COMMON_CHUNK_NAME,
CLASS_DEFINE_CHUNK_NAME,
REACT_CHUNK_NAME,
},
defaultLinkAfter: {
COMMON_DEFAULT_LINK_AFTER: DEFAULT_LINK_AFTER,
},
constants: CONSTANTS,
};
// 一些类型定义
export * from './types';
// 一些常量定义
export * from './const';
// 一些工具函数
export * from './analyzer/componentAnalyzer';
export * from './parser/SchemaParser';
export * from './generator/ChunkBuilder';
export * from './generator/CodeBuilder';
export * from './generator/ModuleBuilder';
export * from './generator/ProjectBuilder';

View File

@ -0,0 +1,355 @@
/**
* 使
* schema
*/
import changeCase from 'change-case';
import {
UtilItem,
NodeDataType,
NodeSchema,
ContainerSchema,
ProjectSchema,
PropsMap,
NodeData,
NpmInfo,
} from '@ali/lowcode-types';
import {
IPageMeta,
CodeGeneratorError,
CompatibilityError,
DependencyType,
IContainerInfo,
IDependency,
IExternalDependency,
IInternalDependency,
InternalDependencyType,
IParseResult,
ISchemaParser,
INpmPackage,
IRouterInfo,
} from '../types';
import { SUPPORT_SCHEMA_VERSION_LIST } from '../const';
import { getErrorMessage } from '../utils/errors';
import { handleSubNodes } from '../utils/schema';
import { uniqueArray } from '../utils/common';
import { componentAnalyzer } from '../analyzer/componentAnalyzer';
import { ensureValidClassName } from '../utils/validate';
const defaultContainer: IContainerInfo = {
containerType: 'Component',
componentName: 'Component',
moduleName: 'Index',
fileName: 'Index',
css: '',
props: {},
};
function getRootComponentName(typeName: string, maps: Record<string, IExternalDependency>): string {
if (maps[typeName]) {
const rec = maps[typeName];
if (rec.destructuring) {
return rec.componentName || typeName;
}
const peerName = Object.keys(maps).find((depName: string) => {
const depInfo = maps[depName];
return (
depName !== typeName &&
!depInfo.destructuring &&
depInfo.package === rec.package &&
depInfo.version === rec.version &&
depInfo.main === rec.main &&
depInfo.exportName === rec.exportName &&
depInfo.subName === rec.subName
);
});
return peerName || typeName;
}
return typeName;
}
function processChildren(schema: NodeSchema): void {
if (schema.props) {
if (Array.isArray(schema.props)) {
// FIXME: is array type props description
} else {
const nodeProps = schema.props as PropsMap;
if (nodeProps.children) {
if (!schema.children) {
// eslint-disable-next-line no-param-reassign
schema.children = nodeProps.children as NodeDataType;
} else {
let _children: NodeData[] = [];
if (Array.isArray(schema.children)) {
_children = _children.concat(schema.children);
} else {
_children.push(schema.children);
}
if (Array.isArray(nodeProps.children)) {
_children = _children.concat(nodeProps.children as NodeData[]);
} else {
_children.push(nodeProps.children as NodeData);
}
// eslint-disable-next-line no-param-reassign
schema.children = _children;
}
delete nodeProps.children;
}
}
}
}
export class SchemaParser implements ISchemaParser {
validate(schema: ProjectSchema): boolean {
if (SUPPORT_SCHEMA_VERSION_LIST.indexOf(schema.version) < 0) {
throw new CompatibilityError(`Not support schema with version [${schema.version}]`);
}
return true;
}
parse(schemaSrc: ProjectSchema | string): IParseResult {
// TODO: collect utils depends in JSExpression
const compDeps: Record<string, IExternalDependency> = {};
const internalDeps: Record<string, IInternalDependency> = {};
let utilsDeps: IExternalDependency[] = [];
const schema = this.decodeSchema(schemaSrc);
// 解析三方组件依赖
schema.componentsMap.forEach((info) => {
if (info.componentName) {
compDeps[info.componentName] = {
...info,
dependencyType: DependencyType.External,
componentName: info.componentName,
exportName: info.exportName ?? info.componentName,
version: info.version || '*',
destructuring: info.destructuring ?? false,
};
}
});
let containers: IContainerInfo[];
// Test if this is a lowcode component without container
if (schema.componentsTree.length > 0) {
const firstRoot: ContainerSchema = schema.componentsTree[0] as ContainerSchema;
if (!('fileName' in firstRoot) || !firstRoot.fileName) {
// 整个 schema 描述一个容器,且无根节点定义
const container: IContainerInfo = {
...firstRoot,
...defaultContainer,
props: firstRoot.props || defaultContainer.props,
css: firstRoot.css || defaultContainer.css,
moduleName: (firstRoot as IContainerInfo).moduleName || defaultContainer.moduleName,
children: schema.componentsTree as NodeSchema[],
};
containers = [container];
} else {
// 普通带 1 到多个容器的 schema
containers = schema.componentsTree.map((n) => {
const subRoot = n as ContainerSchema;
const container: IContainerInfo = {
...subRoot,
componentName: getRootComponentName(subRoot.componentName, compDeps),
containerType: subRoot.componentName,
moduleName: ensureValidClassName(changeCase.pascalCase(subRoot.fileName)),
};
return container;
});
}
} else {
throw new CodeGeneratorError("Can't find anything to generate.");
}
// 分析引用能力的依赖
containers = containers.map((con) => ({
...con,
analyzeResult: componentAnalyzer(con as ContainerSchema),
}));
// 建立所有容器的内部依赖索引
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.moduleName,
destructuring: false,
exportName: container.moduleName,
dependencyType: DependencyType.Internal,
};
internalDeps[dep.moduleName] = dep;
});
const containersDeps = ([] as IDependency[]).concat(...containers.map((c) => c.deps || []));
// TODO: 不应该在出码部分解决?
// 处理 children 写在了 props 里的情况
containers.forEach((container) => {
if (container.children) {
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
handleSubNodes<void>(
container.children,
{
node: (i: NodeSchema) => processChildren(i),
},
{
rerun: true,
},
);
}
});
// 分析容器内部组件依赖
containers.forEach((container) => {
const depNames = this.getComponentNames(container);
// eslint-disable-next-line no-param-reassign
container.deps = uniqueArray<string>(depNames, (i: string) => i)
.map((depName) => internalDeps[depName] || compDeps[depName])
.filter(Boolean);
// container.deps = Object.keys(compDeps).map((depName) => compDeps[depName]);
});
// 分析路由配置
const routes: IRouterInfo['routes'] = containers
.filter((container) => container.containerType === 'Page')
.map((page) => {
const { meta } = page;
if (meta) {
return {
path: (meta as IPageMeta).router || `/${page.fileName}`, // 如果无法找到页面路由信息,则用 fileName 做兜底
fileName: page.fileName,
componentName: page.moduleName,
};
}
return {
path: '',
fileName: page.fileName,
componentName: page.moduleName,
};
});
const routerDeps = routes
.map((r) => internalDeps[r.componentName] || compDeps[r.componentName])
.filter((dep) => !!dep);
// 分析 Utils 依赖
let utils: UtilItem[];
if (schema.utils) {
utils = schema.utils;
utilsDeps = schema.utils
.filter(
(u): u is { name: string; type: 'npm' | 'tnpm'; content: NpmInfo } =>
u.type !== 'function',
)
.map(
(u): IExternalDependency => ({
...u.content,
componentName: u.name,
version: u.content.version || '*',
destructuring: u.content.destructuring ?? false,
exportName: u.content.exportName ?? u.name,
}),
);
} else {
utils = [];
}
// 分析项目 npm 依赖
let npms: INpmPackage[] = [];
containers.forEach((con) => {
const p = (con.deps || [])
.map((dep) => {
return dep.dependencyType === DependencyType.External ? dep : null;
})
.filter((dep) => dep !== null);
const npmInfos: INpmPackage[] = p.filter(Boolean).map((i) => ({
package: (i as IExternalDependency).package,
version: (i as IExternalDependency).version,
}));
npms.push(...npmInfos);
});
npms.push(
...utilsDeps.map((utilsDep) => ({
package: utilsDep.package,
version: utilsDep.version,
})),
);
npms = uniqueArray<INpmPackage>(npms, (i) => i.package).filter(Boolean);
return {
containers,
globalUtils: {
utils,
deps: utilsDeps,
},
globalI18n: schema.i18n,
globalRouter: {
routes,
deps: routerDeps,
},
project: {
css: schema.css,
constants: schema.constants,
config: schema.config || {},
meta: schema.meta || {},
i18n: schema.i18n,
containersDeps,
utilsDeps,
packages: npms || [],
},
};
}
getComponentNames(children: NodeDataType): string[] {
return handleSubNodes<string>(
children,
{
node: (i: NodeSchema) => i.componentName,
},
{
rerun: true,
},
);
}
decodeSchema(schemaSrc: string | ProjectSchema): ProjectSchema {
let schema: ProjectSchema;
if (typeof schemaSrc === 'string') {
try {
schema = JSON.parse(schemaSrc);
} catch (error) {
throw new CodeGeneratorError(
`Parse schema failed: ${getErrorMessage(error) || 'unknown reason'}`,
);
}
} else {
schema = schemaSrc;
}
return schema;
}
}
export default SchemaParser;

View File

@ -0,0 +1,435 @@
import { COMMON_CHUNK_NAME } from '../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
CodeGeneratorError,
DependencyType,
FileType,
ICodeChunk,
ICodeStruct,
IDependency,
IExternalDependency,
IInternalDependency,
IWithDependency,
} from '../../types';
import { isValidIdentifier } from '../../utils/validate';
// TODO: main 这个信息到底怎么用,是不是外部包不需要使用?
const DEP_MAIN_BLOCKLIST = ['lib', 'lib/index', 'es', 'es/index', 'main'];
const DEFAULT_EXPORT_NAME = '__default__';
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.main}` : ''}`, dep);
} else {
let depMain = '';
// TODO: 部分类型的 main 暂时认为没用
if (dep.main && DEP_MAIN_BLOCKLIST.indexOf(dep.main) < 0) {
depMain = dep.main;
}
if (depMain.substring(0, 1) === '/') {
depMain = depMain.substring(1);
}
addDep(`${(dep as IExternalDependency).package}${depMain ? `/${depMain}` : ''}`, dep);
}
});
return depMap;
}
interface IDependencyItem {
exportName: string;
aliasName?: string;
isDefault?: boolean;
subName?: string;
nodeIdentifier?: string; // 与使用处的映射关系,理论上是不可变更的,如需变更需要提供额外信息
source: IDependency;
}
interface IExportItem {
exportName: string;
aliasNames: string[];
isDefault?: boolean;
needOriginExport: boolean;
}
function getDependencyIdentifier(info: IDependencyItem): string {
return info.aliasName || info.exportName;
}
function buildPackageImport(
pkg: string,
deps: IDependency[],
targetFileType: string,
useAliasName: boolean,
): ICodeChunk[] {
const chunks: ICodeChunk[] = [];
const exportItems: Record<string, IExportItem> = {};
const defaultExportNames: string[] = [];
const depsInfo: IDependencyItem[] = deps.map((dep) => {
const info: IDependencyItem = {
exportName: dep.exportName,
isDefault: !dep.destructuring,
subName: dep.subName || undefined,
nodeIdentifier: dep.componentName || undefined,
source: dep,
};
// 下面 5 个逻辑是清理不必要的冗余信息,做到数据结构归一化
if (info.isDefault) {
if (defaultExportNames.indexOf(info.exportName) < 0) {
defaultExportNames.push(info.exportName);
}
}
if (!info.subName) {
if (info.nodeIdentifier === info.exportName) {
info.nodeIdentifier = undefined;
}
if (info.isDefault) {
info.aliasName = info.nodeIdentifier || info.exportName;
info.exportName = DEFAULT_EXPORT_NAME;
}
if (info.nodeIdentifier) {
info.aliasName = info.nodeIdentifier;
info.nodeIdentifier = undefined;
}
} else {
if (info.isDefault) {
info.aliasName = info.exportName;
info.exportName = DEFAULT_EXPORT_NAME;
}
if (info.nodeIdentifier === `${info.exportName}.${info.subName}`) {
info.nodeIdentifier = undefined;
}
}
return info;
});
// 建立 export 项目的列表
depsInfo.forEach((info) => {
if (!exportItems[info.exportName]) {
exportItems[info.exportName] = {
exportName: info.exportName,
isDefault: info.isDefault,
aliasNames: [],
needOriginExport: false,
};
}
if (!info.nodeIdentifier && !info.aliasName) {
exportItems[info.exportName].needOriginExport = true;
}
});
// 建立别名字典
depsInfo.forEach((info) => {
if (info.aliasName) {
const { aliasNames } = exportItems[info.exportName];
if (aliasNames.indexOf(info.aliasName) < 0) {
aliasNames.push(info.aliasName);
}
}
});
// fix: 父组件ImportAliasDefine, 与子组件import的父组件冲突情况
depsInfo.forEach((info) => {
if (info.nodeIdentifier) {
const exportItem = exportItems[info.exportName];
if (!exportItem.needOriginExport && exportItem.aliasNames.length > 0) {
// eslint-disable-next-line no-param-reassign
info.aliasName = exportItem.aliasNames[0];
}
}
});
// 发现 nodeIdentifier 与 exportName 或者 aliasName 冲突的场景
const nodeIdentifiers = depsInfo.map((info) => info.nodeIdentifier).filter(Boolean);
const conflictInfos = Object.keys(exportItems)
.map((exportName) => {
const exportItem = exportItems[exportName];
const usedNames = [
...exportItem.aliasNames,
...(exportItem.needOriginExport || exportItem.aliasNames.length <= 0 ? [exportName] : []),
];
const conflictNames = usedNames.filter((n) => nodeIdentifiers.indexOf(n) >= 0);
if (conflictNames.length > 0) {
return [
...(conflictNames.indexOf(exportName) >= 0 ? [[exportName, true, exportItem]] : []),
...conflictNames.filter((n) => n !== exportName).map((n) => [n, false, exportItem]),
];
}
return [];
})
.flat();
const conflictExports = conflictInfos.filter((c) => c[1]).map((c) => c[0] as string);
const conflictAlias = conflictInfos.filter((c) => !c[1]).map((c) => c[0] as string);
const solutions: Record<string, string> = {};
depsInfo.forEach((info) => {
if (info.aliasName && conflictAlias.indexOf(info.aliasName) >= 0) {
// find solution
let solution = solutions[info.aliasName];
if (!solution) {
solution = `${info.aliasName}Alias`;
const conflictItem = (conflictInfos.find((c) => c[0] === info.aliasName) ||
[])[2] as IExportItem;
conflictItem.aliasNames = conflictItem.aliasNames.filter((a) => a !== info.aliasName);
conflictItem.aliasNames.push(solution);
solutions[info.aliasName] = solution;
}
// eslint-disable-next-line no-param-reassign
info.aliasName = solution;
}
if (conflictExports.indexOf(info.exportName) >= 0) {
// find solution
let solution = solutions[info.exportName];
if (!solution) {
solution = `${info.exportName}Export`;
const conflictItem = (conflictInfos.find((c) => c[0] === info.exportName) ||
[])[2] as IExportItem;
conflictItem.aliasNames.push(solution);
conflictItem.needOriginExport = false;
solutions[info.exportName] = solution;
}
// eslint-disable-next-line no-param-reassign
info.aliasName = solution;
}
});
// 判断是否所有依赖都有合法的 Identifier
depsInfo.forEach((info) => {
const name = info.aliasName || info.exportName;
if (!isValidIdentifier(name)) {
throw new CodeGeneratorError(`Invalid Identifier [${name}]`);
}
if (info.nodeIdentifier && !isValidIdentifier(info.nodeIdentifier)) {
throw new CodeGeneratorError(`Invalid Identifier [${info.nodeIdentifier}]`);
}
});
const aliasDefineStatements: Record<string, string> = {};
if (useAliasName) {
Object.keys(exportItems).forEach((exportName) => {
const aliasList = exportItems[exportName]?.aliasNames || [];
if (aliasList.length > 0) {
const srcName = exportItems[exportName].needOriginExport ? exportName : aliasList[0];
const aliasNameList = exportItems[exportName].needOriginExport
? aliasList
: aliasList.slice(1);
aliasNameList.forEach((a) => {
if (!aliasDefineStatements[a]) {
aliasDefineStatements[a] = `const ${a} = ${srcName};`;
}
});
}
});
}
function getDefaultExportName(info: IDependencyItem): string {
if (info.isDefault) {
return defaultExportNames[0];
}
return info.exportName;
}
depsInfo.forEach((info) => {
// 如果是子组件,则导出父组件,并且根据自组件命名规则,判断是否需要定义标识符
if (info.nodeIdentifier) {
// 前提,存在 nodeIdentifier 一定是有 subName 的,不然前面会优化掉
const ownerName = getDependencyIdentifier(info);
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ImportAliasDefine,
content: useAliasName ? `const ${info.nodeIdentifier} = ${ownerName}.${info.subName};` : '',
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport, COMMON_CHUNK_NAME.InternalDepsImport],
ext: {
originalName: `${getDefaultExportName(info)}.${info.subName}`,
aliasName: info.nodeIdentifier,
dependency: info.source,
},
});
} else if (info.aliasName) {
let contentStatement = '';
if (aliasDefineStatements[info.aliasName]) {
contentStatement = aliasDefineStatements[info.aliasName];
delete aliasDefineStatements[info.aliasName];
}
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ImportAliasDefine,
content: contentStatement,
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport, COMMON_CHUNK_NAME.InternalDepsImport],
ext: {
originalName: getDefaultExportName(info),
aliasName: info.aliasName,
dependency: info.source,
},
});
}
});
// 可能会剩余一些存在二次转换的定义
Object.keys(aliasDefineStatements).forEach((a) => {
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ImportAliasDefine,
content: aliasDefineStatements[a],
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport, COMMON_CHUNK_NAME.InternalDepsImport],
});
});
const exportItemList = Object.keys(exportItems).map((k) => exportItems[k]);
const defaultExport = exportItemList.filter((item) => item.isDefault);
const otherExports = exportItemList.filter((item) => !item.isDefault);
const statementL = ['import'];
if (defaultExport.length > 0) {
if (useAliasName) {
statementL.push(defaultExportNames[0]);
} else {
statementL.push(defaultExport[0].aliasNames[0]);
}
if (otherExports.length > 0) {
statementL.push(', ');
}
}
if (otherExports.length > 0) {
const items = otherExports.map((item) => {
return !useAliasName || item.needOriginExport || item.aliasNames.length <= 0
? item.exportName
: `${item.exportName} as ${item.aliasNames[0]}`;
});
statementL.push(`{ ${items.join(', ')} }`);
}
statementL.push('from');
const getInternalDependencyModuleId = () => `@/${(deps[0] as IInternalDependency).type}/${pkg}`;
if (deps[0].dependencyType === DependencyType.Internal) {
// TODO: Internal Deps path use project slot setting
statementL.push(`'${getInternalDependencyModuleId()}';`);
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.InternalDepsImport,
content: statementL.join(' '),
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport],
});
} else {
statementL.push(`'${pkg}';`);
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: statementL.join(' '),
linkAfter: [],
});
}
// 处理下一些额外的 default 方式的导入
if (defaultExportNames.length > 1) {
if (deps[0].dependencyType === DependencyType.Internal) {
defaultExportNames.slice(1).forEach((exportName) => {
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.InternalDepsImport,
content: `import ${exportName} from '${getInternalDependencyModuleId()}';`,
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport],
});
});
} else {
defaultExportNames.slice(1).forEach((exportName) => {
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: `import ${exportName} from '${pkg}';`,
linkAfter: [],
});
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ImportAliasDefine,
content: '',
linkAfter: [],
ext: {
aliasName: exportName,
originalName: exportName,
dependency: {
package: pkg,
componentName: exportName,
},
},
});
});
}
}
return chunks;
}
export interface PluginConfig {
fileType?: string; // 导出的文件类型
useAliasName?: boolean; // 是否使用 componentName 重命名组件 identifier
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?: PluginConfig) => {
const cfg = {
fileType: FileType.JS,
useAliasName: true,
...(config || {}),
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IWithDependency;
if (ir && ir.deps && ir.deps.length > 0) {
const packs = groupDepsByPack(ir.deps);
Object.keys(packs).forEach((pkg) => {
const chunks = buildPackageImport(pkg, packs[pkg], cfg.fileType, cfg.useAliasName);
next.chunks.push(...chunks);
});
}
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,25 @@
import { COMMON_CHUNK_NAME } from '../../const/generator';
import { BuilderComponentPlugin, BuilderComponentPluginFactory, ChunkType, FileType, ICodeStruct } from '../../types';
// TODO: How to merge this logic to common deps
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
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;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,35 @@
import { COMMON_CHUNK_NAME } from '../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
} from '../../../types';
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
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: `
// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。
// 例外rax 框架的导出名和各种组件名除外。
import { createElement, Component } from 'rax';
import { getSearchParams as __$$getSearchParams } from 'rax-app';
`,
linkAfter: [],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,18 @@
export const RAX_CHUNK_NAME = {
ClassDidMountBegin: 'RaxComponentClassDidMountBegin',
ClassDidMountContent: 'RaxComponentClassDidMountContent',
ClassDidMountEnd: 'RaxComponentClassDidMountEnd',
ClassWillUnmountBegin: 'RaxComponentClassWillUnmountBegin',
ClassWillUnmountContent: 'RaxComponentClassWillUnmountContent',
ClassWillUnmountEnd: 'RaxComponentClassWillUnmountEnd',
ClassRenderBegin: 'RaxComponentClassRenderBegin',
ClassRenderPre: 'RaxComponentClassRenderPre',
ClassRenderJSX: 'RaxComponentClassRenderJSX',
ClassRenderEnd: 'RaxComponentClassRenderEnd',
MethodsBegin: 'RaxComponentMethodsBegin',
MethodsContent: 'RaxComponentMethodsContent',
MethodsEnd: 'RaxComponentMethodsEnd',
LifeCyclesBegin: 'RaxComponentLifeCyclesBegin',
LifeCyclesContent: 'RaxComponentLifeCyclesContent',
LifeCyclesEnd: 'RaxComponentLifeCyclesEnd',
};

View File

@ -0,0 +1,170 @@
import changeCase from 'change-case';
import {
COMMON_CHUNK_NAME,
CLASS_DEFINE_CHUNK_NAME,
DEFAULT_LINK_AFTER,
} from '../../../const/generator';
import { RAX_CHUNK_NAME } from './const';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IContainerInfo,
} from '../../../types';
import { ensureValidClassName } from '../../../utils/validate';
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
// 将模块名转换成 PascalCase 的格式,并添加特定后缀,防止命名冲突
const componentClassName = ensureValidClassName(
`${changeCase.pascalCase(ir.moduleName)}$$Page`,
);
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: CLASS_DEFINE_CHUNK_NAME.Start,
content: `class ${componentClassName} extends Component {`,
linkAfter: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
COMMON_CHUNK_NAME.FileUtilDefine,
],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: CLASS_DEFINE_CHUNK_NAME.End,
content: '}',
linkAfter: [
CLASS_DEFINE_CHUNK_NAME.Start,
CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod,
RAX_CHUNK_NAME.ClassRenderEnd,
RAX_CHUNK_NAME.MethodsEnd,
],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: CLASS_DEFINE_CHUNK_NAME.ConstructorStart,
content: 'constructor(props, context) { super(props); ',
linkAfter: DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.ConstructorStart],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: CLASS_DEFINE_CHUNK_NAME.ConstructorEnd,
content: '} /* end of constructor */',
linkAfter: DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.ConstructorEnd],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: RAX_CHUNK_NAME.ClassDidMountBegin,
content: 'componentDidMount() {',
linkAfter: [
CLASS_DEFINE_CHUNK_NAME.Start,
CLASS_DEFINE_CHUNK_NAME.InsVar,
CLASS_DEFINE_CHUNK_NAME.InsMethod,
CLASS_DEFINE_CHUNK_NAME.ConstructorEnd,
],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: RAX_CHUNK_NAME.ClassDidMountEnd,
content: '} /* end of componentDidMount */',
linkAfter: [RAX_CHUNK_NAME.ClassDidMountBegin, RAX_CHUNK_NAME.ClassDidMountContent],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: RAX_CHUNK_NAME.ClassWillUnmountBegin,
content: 'componentWillUnmount() {',
linkAfter: [
CLASS_DEFINE_CHUNK_NAME.Start,
CLASS_DEFINE_CHUNK_NAME.InsVar,
CLASS_DEFINE_CHUNK_NAME.InsMethod,
RAX_CHUNK_NAME.ClassDidMountEnd,
],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: RAX_CHUNK_NAME.ClassWillUnmountEnd,
content: '} /* end of componentWillUnmount */',
linkAfter: [RAX_CHUNK_NAME.ClassWillUnmountBegin, RAX_CHUNK_NAME.ClassWillUnmountContent],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: RAX_CHUNK_NAME.ClassRenderBegin,
content: 'render() {',
linkAfter: [RAX_CHUNK_NAME.ClassDidMountEnd, RAX_CHUNK_NAME.ClassWillUnmountEnd],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: RAX_CHUNK_NAME.ClassRenderEnd,
content: '} /* end of render */',
linkAfter: [
RAX_CHUNK_NAME.ClassRenderBegin,
RAX_CHUNK_NAME.ClassRenderPre,
RAX_CHUNK_NAME.ClassRenderJSX,
],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod,
content: `
_i18nText(t) {
const locale = this._context.getLocale();
return t[locale] ?? t[String(locale).replace('-', '_')] ?? t[t.use || 'zh_CN'] ?? t.en_US;
}
`,
linkAfter: [RAX_CHUNK_NAME.ClassRenderEnd],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: COMMON_CHUNK_NAME.FileExport,
content: `export default ${componentClassName};`,
linkAfter: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
COMMON_CHUNK_NAME.FileUtilDefine,
CLASS_DEFINE_CHUNK_NAME.End,
],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,66 @@
import { CLASS_DEFINE_CHUNK_NAME, DEFAULT_LINK_AFTER } from '../../../const/generator';
import { generateCompositeType } from '../../../utils/compositeType';
import Scope from '../../../utils/Scope';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IContainerInfo,
} from '../../../types';
export interface PluginConfig {
fileType: string;
implementType: 'inConstructor' | 'insMember' | 'hooks';
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
implementType: 'insMember',
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
const scope = Scope.createRootScope();
const state = ir.state || {};
const fields = Object.keys(state).map<string>((stateName) => {
// TODO: 这里用什么 handlers?
const value = generateCompositeType(state[stateName], scope);
return `${JSON.stringify(stateName)}: ${value}`;
});
if (cfg.implementType === 'inConstructor') {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.ConstructorContent,
content: `this.state = { ${fields.join(',')} };`,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.ConstructorContent]],
});
} else if (cfg.implementType === 'insMember') {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsVar,
content: `state = { ${fields.join(',')} };`,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsVar]],
});
}
// TODO: hooks state??
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,134 @@
/* eslint-disable @typescript-eslint/indent */
import { CLASS_DEFINE_CHUNK_NAME, COMMON_CHUNK_NAME } from '../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IContainerInfo,
} from '../../../types';
import { RAX_CHUNK_NAME } from './const';
export interface PluginConfig {
fileType: string;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
const useRef = !!ir.analyzeResult?.isUsingRef;
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.InternalDepsImport,
content: "import __$$constants from '../../constants';",
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport],
});
// TODO: i18n 是可选的,如果没有 i18n 这个文件怎么办?该怎么判断?
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.InternalDepsImport,
content: "import * as __$$i18n from '../../i18n';",
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsVar,
content: `
_context = this._createContext();
`,
linkAfter: [CLASS_DEFINE_CHUNK_NAME.Start],
});
// TODO: 按照目前的实现方案,代码的插拔能力太弱了,需要有一些变化。
// Step 1: 增加前置的分析器
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod,
content: `
_createContext() {
const self = this;
const context = {
get state() {
return self.state;
},
setState(newState, callback) {
self.setState(newState, callback);
},
get dataSourceMap() {
return self._dataSourceEngine.dataSourceMap || {};
},
async reloadDataSource() {
await self._dataSourceEngine.reloadDataSource();
},
get utils() {
return self._utils;
},
get page() {
return context;
},
get component() {
return context;
},
get props() {
return self.props;
},
get constants() {
return __$$constants;
},
i18n: __$$i18n.i18n,
i18nFormat: __$$i18n.i18nFormat,
getLocale: __$$i18n.getLocale,
setLocale(locale) {
__$$i18n.setLocale(locale);
self.forceUpdate();
},${
useRef
? `
$(refName) {
return self._refsManager.get(refName);
},
$$(refName) {
return self._refsManager.getAll(refName);
},
get _refsManager() {
if (!self._refsManager) {
self._refsManager = new RefsManager();
}
return self._refsManager;
},
`
: ''
}
...this._methods,
};
return context;
}
`,
linkAfter: [RAX_CHUNK_NAME.ClassRenderEnd],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,188 @@
/* eslint-disable @typescript-eslint/indent */
import {
CompositeValue,
JSExpression,
InterpretDataSourceConfig,
isJSExpression,
isJSFunction,
} from '@ali/lowcode-types';
import changeCase from 'change-case';
import { CLASS_DEFINE_CHUNK_NAME, COMMON_CHUNK_NAME } from '../../../const/generator';
import Scope from '../../../utils/Scope';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IScope,
} from '../../../types';
import { generateCompositeType } from '../../../utils/compositeType';
import { parseExpressionConvertThis2Context } from '../../../utils/expressionParser';
import { isContainerSchema } from '../../../utils/schema';
import { RaxFrameworkOptions } from '../../project/framework/rax/types/RaxFrameworkOptions';
import { RAX_CHUNK_NAME } from './const';
export interface PluginConfig extends RaxFrameworkOptions {
fileType?: string;
dataSourceHandlersPackageMap?: Record<string, string>;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
...config,
dataSourceHandlersPackageMap:
config?.dataSourceHandlersPackageMap || config?.datasourceConfig?.handlersPackages,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const scope = Scope.createRootScope();
const dataSourceConfig = isContainerSchema(pre.ir) ? pre.ir.dataSource : null;
const dataSourceItems: InterpretDataSourceConfig[] =
(dataSourceConfig && dataSourceConfig.list) || [];
const dataSourceEngineOptions = { runtimeConfig: true };
if (dataSourceItems.length > 0) {
const requestHandlersMap: Record<string, JSExpression> = {};
dataSourceItems.forEach((ds) => {
const dsType = ds.type || 'fetch';
if (!(dsType in requestHandlersMap) && dsType !== 'custom') {
const handlerFactoryName = `__$$create${changeCase.pascal(dsType)}RequestHandler`;
requestHandlersMap[dsType] = {
type: 'JSExpression',
value: `${handlerFactoryName}(${
dsType === 'urlParams' ? '__$$getSearchParams()' : ''
})`,
};
const handlerFactoryExportName = `create${changeCase.pascal(dsType)}Handler`;
const handlerPkgName =
cfg.dataSourceHandlersPackageMap?.[dsType] ||
`@ali/lowcode-datasource-${changeCase.kebab(dsType)}-handler`;
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: `
import { ${handlerFactoryExportName} as ${handlerFactoryName} } from '${handlerPkgName}';
`,
linkAfter: [],
});
}
});
Object.assign(dataSourceEngineOptions, { requestHandlersMap });
}
const datasourceEnginePackageName =
cfg.datasourceConfig?.enginePackage || '@ali/lowcode-datasource-engine';
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: `
import { create as __$$createDataSourceEngine } from '${datasourceEnginePackageName}/runtime';
`,
linkAfter: [],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType!,
name: CLASS_DEFINE_CHUNK_NAME.InsVar,
content: `
_dataSourceConfig = this._defineDataSourceConfig();
_dataSourceEngine = __$$createDataSourceEngine(
this._dataSourceConfig,
this._context,
${generateCompositeType(dataSourceEngineOptions, scope)}
);`,
linkAfter: [CLASS_DEFINE_CHUNK_NAME.Start],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType!,
name: RAX_CHUNK_NAME.ClassDidMountContent,
content: `
this._dataSourceEngine.reloadDataSource();
`,
linkAfter: [RAX_CHUNK_NAME.ClassDidMountBegin],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType!,
name: CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod,
content: `
_defineDataSourceConfig() {
const __$$context = this._context;
return (${generateCompositeType(
{
...dataSourceConfig,
list: [
...dataSourceItems.map((item) => ({
// 数据源引擎默认的 errorHandler 是空的,而且并不会触发组件重新渲染……
// 这会导致页面状态不能正常展示,故这里处理下:
errorHandler: {
type: 'JSFunction',
value: `function (err){
setTimeout(() => {
this.setState({ __refresh: Date.now() + Math.random() });
}, 0);
throw err;
}`,
},
...item,
isInit:
typeof item.isInit === 'boolean' || typeof item.isInit === 'undefined'
? item.isInit ?? true
: wrapAsFunction(item.isInit, scope),
options: wrapAsFunction(item.options, scope),
})),
],
},
scope,
{
handlers: {
function: (jsFunc) => parseExpressionConvertThis2Context(jsFunc.value, '__$$context'),
expression: (jsExpr) => parseExpressionConvertThis2Context(jsExpr.value, '__$$context'),
},
},
)});
}
`,
linkAfter: [RAX_CHUNK_NAME.ClassRenderEnd],
});
return next;
};
return plugin;
};
export default pluginFactory;
function wrapAsFunction(value: CompositeValue, scope: IScope): CompositeValue {
if (isJSExpression(value) || isJSFunction(value)) {
return {
type: 'JSExpression',
value: `function(){ return ((${value.value}))}`,
};
}
return {
type: 'JSExpression',
value: `function(){return((${generateCompositeType(value, scope)}))}`,
};
}

View File

@ -0,0 +1,69 @@
import { CLASS_DEFINE_CHUNK_NAME, COMMON_CHUNK_NAME } from '../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IContainerInfo,
} from '../../../types';
import { RAX_CHUNK_NAME } from './const';
export interface PluginConfig {
fileType: string;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
const useRef = !!ir.analyzeResult?.isUsingRef;
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.InternalDepsImport,
// TODO: 下面这个路径有没有更好的方式来获取?而非写死
content: `
import __$$projectUtils${useRef ? ', { RefsManager }' : ''} from '../../utils';
`,
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsVar,
content: '_utils = this._defineUtils();',
linkAfter: [CLASS_DEFINE_CHUNK_NAME.Start],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod,
content: `
_defineUtils() {
return {
...__$$projectUtils,
};
}`,
linkAfter: [RAX_CHUNK_NAME.ClassRenderEnd],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,144 @@
import _ from 'lodash';
import { isJSExpression, isJSFunction } from '@ali/lowcode-types';
import { CLASS_DEFINE_CHUNK_NAME } from '../../../const/generator';
import { RAX_CHUNK_NAME } from './const';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
FileType,
ChunkType,
ICodeStruct,
IContainerInfo,
} from '../../../types';
import { debug } from '../../../utils/debug';
export interface PluginConfig {
fileType: string;
exportNameMapping: Record<string, string>;
normalizeNameMapping: Record<string, string>;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
exportNameMapping: {},
normalizeNameMapping: {
didMount: 'componentDidMount',
willUnmount: 'componentWillUnmount',
},
...config,
};
const exportNameMapping = new Map(Object.entries(cfg.exportNameMapping));
const normalizeNameMapping = new Map(Object.entries(cfg.normalizeNameMapping));
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
// Rax 先只支持 didMount 和 willUnmount 吧
const ir = next.ir as IContainerInfo;
const { lifeCycles } = ir;
if (lifeCycles && !_.isEmpty(lifeCycles)) {
Object.entries(lifeCycles).forEach(([lifeCycleName, lifeCycleMethodExpr]) => {
// 过滤掉非法数据(有些场景下会误传入空字符串或 null)
if (
!isJSFunction(lifeCycles[lifeCycleName]) &&
!isJSExpression(lifeCycles[lifeCycleName])
) {
return;
}
const normalizeName = normalizeNameMapping.get(lifeCycleName) || lifeCycleName;
const exportName = exportNameMapping.get(lifeCycleName) || lifeCycleName;
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.LifeCyclesContent,
content: `${exportName}: (${lifeCycleMethodExpr.value}),`,
linkAfter: [RAX_CHUNK_NAME.LifeCyclesBegin],
});
if (normalizeName === 'constructor') {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.ConstructorContent,
content: `this._lifeCycles.${exportName}();`,
linkAfter: [CLASS_DEFINE_CHUNK_NAME.ConstructorStart],
});
} else if (normalizeName === 'componentDidMount') {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.ClassDidMountContent,
content: `this._lifeCycles.${exportName}();`,
linkAfter: [RAX_CHUNK_NAME.ClassDidMountBegin],
});
} else if (normalizeName === 'componentWillUnmount') {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.ClassWillUnmountContent,
content: `this._lifeCycles.${exportName}();`,
linkAfter: [RAX_CHUNK_NAME.ClassWillUnmountBegin],
});
} else {
debug(`[CodeGen]: unknown life cycle: ${lifeCycleName}`);
}
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsVar,
content: '_lifeCycles = this._defineLifeCycles();',
linkAfter: [CLASS_DEFINE_CHUNK_NAME.Start],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.LifeCyclesBegin,
content: `
_defineLifeCycles() {
const __$$lifeCycles = ({
`,
linkAfter: [RAX_CHUNK_NAME.ClassRenderEnd, CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.LifeCyclesEnd,
content: `
});
// 为所有的方法绑定上下文
Object.entries(__$$lifeCycles).forEach(([lifeCycleName, lifeCycleMethod]) => {
if (typeof lifeCycleMethod === 'function') {
__$$lifeCycles[lifeCycleName] = (...args) => {
return lifeCycleMethod.apply(this._context, args);
}
}
});
return __$$lifeCycles;
}
`,
linkAfter: [RAX_CHUNK_NAME.LifeCyclesBegin, RAX_CHUNK_NAME.LifeCyclesContent],
});
}
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,84 @@
import { CLASS_DEFINE_CHUNK_NAME } from '../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IContainerInfo,
} from '../../../types';
import { RAX_CHUNK_NAME } from './const';
export interface PluginConfig {
fileType: string;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsVar,
content: `
_methods = this._defineMethods();
`,
linkAfter: [CLASS_DEFINE_CHUNK_NAME.Start],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.MethodsBegin,
content: `
_defineMethods() {
return ({
`,
linkAfter: [
RAX_CHUNK_NAME.ClassRenderEnd,
CLASS_DEFINE_CHUNK_NAME.InsPrivateMethod,
RAX_CHUNK_NAME.LifeCyclesEnd,
],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.MethodsEnd,
content: `
});
}
`,
linkAfter: [RAX_CHUNK_NAME.MethodsBegin, RAX_CHUNK_NAME.MethodsContent],
});
if (ir.methods && Object.keys(ir.methods).length > 0) {
Object.entries(ir.methods).forEach(([methodName, methodDefine]) => {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.MethodsContent,
content: `${methodName}: (${methodDefine.value}),`,
linkAfter: [RAX_CHUNK_NAME.MethodsBegin],
});
});
}
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,399 @@
import {
NodeSchema,
JSExpression,
NpmInfo,
CompositeValue,
isJSExpression,
} from '@ali/lowcode-types';
import _ from 'lodash';
import changeCase from 'change-case';
import { Expression } from '@babel/types';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
CodePiece,
FileType,
ICodeChunk,
ICodeStruct,
IContainerInfo,
PIECE_TYPE,
HandlerSet,
IScope,
NodeGeneratorConfig,
NodePlugin,
AttrPlugin,
} from '../../../types';
import { RAX_CHUNK_NAME } from './const';
import { COMMON_CHUNK_NAME } from '../../../const/generator';
import { generateExpression } from '../../../utils/jsExpression';
import {
createNodeGenerator,
generateConditionReactCtrl,
generateReactExprInJS,
} from '../../../utils/nodeToJSX';
import { generateCompositeType } from '../../../utils/compositeType';
import Scope from '../../../utils/Scope';
import {
parseExpression,
parseExpressionConvertThis2Context,
parseExpressionGetGlobalVariables,
} from '../../../utils/expressionParser';
export interface PluginConfig {
fileType: string;
/** 是否要忽略小程序 */
ignoreMiniApp?: boolean;
}
// TODO: componentName 若并非大写字符打头,甚至并非是一个有效的 JS 标识符怎么办??
// FIXME: 我想了下,这块应该放到解析阶段就去做掉,对所有 componentName 做 identifier validate然后对不合法的做统一替换。
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
const rootScope = Scope.createRootScope();
// Rax 构建到小程序的时候,不能给组件起起别名,得直接引用,故这里将所有的别名替换掉
// 先收集下所有的 alias 的映射
const componentsNameAliasMap = new Map<string, string>();
next.chunks.forEach((chunk) => {
if (isImportAliasDefineChunk(chunk)) {
componentsNameAliasMap.set(chunk.ext.aliasName, chunk.ext.originalName);
}
});
// 注意这里其实隐含了一个假设schema 中的 componentName 应该是一个有效的 JS 标识符,而且是大写字母打头的
// FIXME: 为了快速修复临时加的逻辑,需要用 pre-process 的方式替代处理。
const mapComponentNameToAliasOrKeepIt = (componentName: string) =>
componentsNameAliasMap.get(componentName) || componentName;
// 然后过滤掉所有的别名 chunks
next.chunks = next.chunks.filter((chunk) => !isImportAliasDefineChunk(chunk));
// 如果直接按目前的 React 的方式之间出码 JSX 的话,会有 3 个问题:
// 1. 小程序出码的时候,循环变量没法拿到
// 2. 小程序出码的时候,很容易出现 Uncaught TypeError: Cannot read property 'avatar' of undefined 这样的异常(如下图的 50 行) -- 因为若直接出码Rax 构建到小程序的时候会立即计算所有在视图中用到的变量
// 3. 通过 this.xxx 能拿到的东西太多了,而且自定义的 methods 可能会无意间破坏 Rax 框架或小程序框架在页面 this 上的东东
const customHandlers: HandlerSet<string> = {
expression(input: JSExpression, scope: IScope) {
return transformJsExpr(generateExpression(input, scope), scope);
},
function(input, scope: IScope) {
return transformThis2Context(input.value || 'null', scope);
},
};
// 创建代码生成器
const commonNodeGenerator = createNodeGenerator({
handlers: customHandlers,
tagMapping: mapComponentNameToAliasOrKeepIt,
nodePlugins: [generateReactExprInJS, generateConditionReactCtrl, generateRaxLoopCtrl],
attrPlugins: [generateNodeAttrForRax.bind({ cfg })],
});
// 生成 JSX 代码
const jsxContent = commonNodeGenerator(ir, rootScope);
if (!cfg.ignoreMiniApp) {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: "import { isMiniApp as __$$isMiniApp } from 'universal-env';",
linkAfter: [],
});
}
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.ClassRenderPre,
// TODO: setState, dataSourceMap, reloadDataSource, utils, i18n, i18nFormat, getLocale, setLocale 这些在 Rax 的编译模式下不能在视图中直接访问,需要转化成 this.xxx
content: `
const __$$context = this._context;
const { state, setState, dataSourceMap, reloadDataSource, utils, constants, i18n, i18nFormat, getLocale, setLocale } = __$$context;
`,
linkAfter: [RAX_CHUNK_NAME.ClassRenderBegin],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: RAX_CHUNK_NAME.ClassRenderJSX,
content: `return ${jsxContent};`,
linkAfter: [RAX_CHUNK_NAME.ClassRenderBegin, RAX_CHUNK_NAME.ClassRenderPre],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.CustomContent,
content: `
function __$$eval(expr) {
try {
return expr();
} catch (err) {
try {
if (window.handleEvalError) {
window.handleEvalError('Failed to evaluate: ', expr, err);
}
} catch (e) {}
}
}
function __$$evalArray(expr) {
const res = __$$eval(expr);
return Array.isArray(res) ? res : [];
}
function __$$createChildContext(oldContext, ext) {
return Object.assign({}, oldContext, ext);
}
`,
linkAfter: [COMMON_CHUNK_NAME.FileExport],
});
return next;
};
return plugin;
};
export default pluginFactory;
function transformJsExpr(expr: string, scope: IScope) {
if (!expr) {
return 'undefined';
}
if (isLiteralAtomicExpr(expr)) {
return expr;
}
const exprAst = parseExpression(expr);
// 对于下面这些比较安全的字面值,可以直接返回对应的表达式,而非包一层
if (isSimpleStraightLiteral(exprAst)) {
return expr;
}
switch (exprAst.type) {
// 对于直接写个函数的,则不用再包下,因为这样不会抛出异常的
case 'ArrowFunctionExpression':
case 'FunctionExpression':
return transformThis2Context(exprAst, scope);
default:
break;
}
// 其他的都需要包一层
return `__$$eval(() => (${transformThis2Context(exprAst, scope)}))`;
}
/** 判断是非是一些简单直接的字面值 */
function isSimpleStraightLiteral(expr: Expression): boolean {
switch (expr.type) {
case 'BigIntLiteral':
case 'BooleanLiteral':
case 'DecimalLiteral':
case 'NullLiteral':
case 'NumericLiteral':
case 'RegExpLiteral':
case 'StringLiteral':
return true;
default:
return false;
}
}
function isImportAliasDefineChunk(chunk: ICodeChunk): chunk is ICodeChunk & {
ext: {
aliasName: string;
originalName: string;
dependency: NpmInfo;
};
} {
return (
chunk.name === COMMON_CHUNK_NAME.ImportAliasDefine &&
!!chunk.ext &&
typeof chunk.ext.aliasName === 'string' &&
typeof chunk.ext.originalName === 'string' &&
!!(chunk.ext.dependency as NpmInfo | null)?.componentName
);
}
/**
*
*/
function isLiteralAtomicExpr(expr: string): boolean {
return (
expr === 'null' ||
expr === 'undefined' ||
expr === 'true' ||
expr === 'false' ||
/^-?\d+(\.\d+)?$/.test(expr)
);
}
/**
* this.xxx __$$context.xxx
* @param expr
*/
function transformThis2Context(expr: string | Expression, scope: IScope): string {
// 下面这种字符串替换的方式虽然简单直接,但是对于复杂场景会误匹配,故后期改成了解析 AST 然后修改 AST 最后再重新生成代码的方式
// return expr
// .replace(/\bthis\.item\./g, () => 'item.')
// .replace(/\bthis\.index\./g, () => 'index.')
// .replace(/\bthis\./g, () => '__$$context.');
return parseExpressionConvertThis2Context(
expr,
'__$$context',
scope.bindings?.getAllBindings() || [],
);
}
function generateRaxLoopCtrl(
nodeItem: NodeSchema,
scope: IScope,
config?: NodeGeneratorConfig,
next?: NodePlugin,
): CodePiece[] {
if (nodeItem.loop) {
const loopItemName = nodeItem.loopArgs?.[0] || 'item';
const loopIndexName = nodeItem.loopArgs?.[1] || 'index';
const subScope = scope.createSubScope([loopItemName, loopIndexName]);
const pieces: CodePiece[] = next ? next(nodeItem, subScope, config) : [];
const loopDataExpr = `__$$evalArray(() => (${transformThis2Context(
generateCompositeType(nodeItem.loop, scope, { handlers: config?.handlers }),
scope,
)}))`;
pieces.unshift({
value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => ((__$$context) => (`,
type: PIECE_TYPE.BEFORE,
});
pieces.push({
value: `))(__$$createChildContext(__$$context, { ${loopItemName}, ${loopIndexName} })))`,
type: PIECE_TYPE.AFTER,
});
return pieces;
}
return next ? next(nodeItem, scope, config) : [];
}
function generateNodeAttrForRax(
this: { cfg: PluginConfig },
attrData: { attrName: string; attrValue: CompositeValue },
scope: IScope,
config?: NodeGeneratorConfig,
next?: AttrPlugin,
): CodePiece[] {
if (!this.cfg.ignoreMiniApp && /^on/.test(attrData.attrName)) {
// else: onXxx 的都是事件处理函数需要特殊处理下
return generateEventHandlerAttrForRax(attrData.attrName, attrData.attrValue, scope, config);
}
if (attrData.attrName === 'ref') {
return [
{
name: attrData.attrName,
value: `__$$context._refsManager.linkRef('${attrData.attrValue}')`,
type: PIECE_TYPE.ATTR,
},
];
}
return next ? next(attrData, scope, config) : [];
}
function generateEventHandlerAttrForRax(
attrName: string,
attrValue: CompositeValue,
scope: IScope,
config?: NodeGeneratorConfig,
): CodePiece[] {
// -- 事件处理函数中 JSExpression 转成 JSFunction 来处理,避免当 JSExpression 处理的时候多包一层 eval 而导致 Rax 转码成小程序的时候出问题
const valueExpr = generateCompositeType(
isJSExpression(attrValue) ? { type: 'JSFunction', value: attrValue.value } : attrValue,
scope,
{
handlers: config?.handlers,
},
);
// 查询当前作用域下的变量
const currentScopeVariables = scope.bindings?.getAllBindings() || [];
if (currentScopeVariables.length <= 0) {
return [
{
type: PIECE_TYPE.ATTR,
name: attrName,
value: valueExpr,
},
];
}
// 提取出所有的未定义的全局变量
const undeclaredVariablesInValueExpr = parseExpressionGetGlobalVariables(valueExpr);
const referencedLocalVariables = _.intersection(
undeclaredVariablesInValueExpr,
currentScopeVariables,
);
if (referencedLocalVariables.length <= 0) {
return [
{
type: PIECE_TYPE.ATTR,
name: attrName,
value: valueExpr,
},
];
}
const wrappedAttrValueExpr = [
'(...__$$args) => {',
' if (__$$isMiniApp) {',
' const __$$event = __$$args[0];',
...referencedLocalVariables.map(
(localVar) => `const ${localVar} = __$$event.target.dataset.${localVar};`,
),
` return (${valueExpr}).apply(this, __$$args);`,
' } else {',
` return (${valueExpr}).apply(this, __$$args);`,
' }',
'}',
].join('\n');
return [
...referencedLocalVariables.map((localVar) => ({
type: PIECE_TYPE.ATTR,
name: `data-${changeCase.snake(localVar)}`,
value: localVar,
})),
{
type: PIECE_TYPE.ATTR,
name: attrName,
value: wrappedAttrValueExpr,
},
];
}

View File

@ -0,0 +1,9 @@
export const REACT_CHUNK_NAME = {
ClassRenderStart: 'ReactComponentClassRenderStart',
ClassRenderPre: 'ReactComponentClassRenderPre',
ClassRenderEnd: 'ReactComponentClassRenderEnd',
ClassRenderJSX: 'ReactComponentClassRenderJSX',
ClassDidMountStart: 'ReactComponentClassDidMountStart',
ClassDidMountEnd: 'ReactComponentClassDidMountEnd',
ClassDidMountContent: 'ReactComponentClassDidMountContent',
};

View File

@ -0,0 +1,133 @@
import changeCase from 'change-case';
import {
COMMON_CHUNK_NAME,
CLASS_DEFINE_CHUNK_NAME,
DEFAULT_LINK_AFTER,
} from '../../../const/generator';
import { REACT_CHUNK_NAME } from './const';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IContainerInfo,
} from '../../../types';
import { ensureValidClassName } from '../../../utils/validate';
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
// 将模块名转换成 PascalCase 的格式,并添加特定后缀,防止命名冲突
const componentClassName = ensureValidClassName(
`${changeCase.pascalCase(ir.moduleName)}$$Page`,
);
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: CLASS_DEFINE_CHUNK_NAME.Start,
content: `class ${componentClassName} extends React.Component {`,
linkAfter: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
COMMON_CHUNK_NAME.FileUtilDefine,
],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: CLASS_DEFINE_CHUNK_NAME.End,
content: '}',
linkAfter: [
...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.End],
REACT_CHUNK_NAME.ClassRenderEnd,
REACT_CHUNK_NAME.ClassDidMountEnd,
],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: CLASS_DEFINE_CHUNK_NAME.ConstructorStart,
content: 'constructor(props, context) { super(props); ',
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.ConstructorStart]],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: CLASS_DEFINE_CHUNK_NAME.ConstructorEnd,
content: '}',
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.ConstructorEnd]],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: REACT_CHUNK_NAME.ClassDidMountStart,
content: 'componentDidMount() {',
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.End]],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: REACT_CHUNK_NAME.ClassDidMountEnd,
content: '}',
linkAfter: [REACT_CHUNK_NAME.ClassDidMountContent, REACT_CHUNK_NAME.ClassDidMountStart],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: REACT_CHUNK_NAME.ClassRenderStart,
content: 'render() {',
linkAfter: [
...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.End],
REACT_CHUNK_NAME.ClassDidMountEnd,
],
});
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 ${componentClassName};`,
linkAfter: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
COMMON_CHUNK_NAME.FileUtilDefine,
CLASS_DEFINE_CHUNK_NAME.End,
],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,63 @@
import { CLASS_DEFINE_CHUNK_NAME, DEFAULT_LINK_AFTER } from '../../../const/generator';
import { generateCompositeType } from '../../../utils/compositeType';
import Scope from '../../../utils/Scope';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IContainerInfo,
} from '../../../types';
export interface PluginConfig {
fileType: string;
implementType: 'inConstructor' | 'insMember' | 'hooks';
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
implementType: 'inConstructor',
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
const scope = Scope.createRootScope();
const state = ir.state || {};
const fields = Object.keys(state).map<string>((stateName) => {
const value = generateCompositeType(state[stateName], scope);
return `${stateName}: ${value},`;
});
if (cfg.implementType === 'inConstructor') {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.ConstructorContent,
content: `this.state = { ${fields.join('')} };`,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.ConstructorContent]],
});
} else if (cfg.implementType === 'insMember') {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsVar,
content: `state = { ${fields.join('')} };`,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsVar]],
});
}
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,179 @@
/* eslint-disable @typescript-eslint/indent */
import {
CompositeValue,
JSExpression,
InterpretDataSourceConfig,
isJSExpression,
isJSFunction,
} from '@ali/lowcode-types';
import changeCase from 'change-case';
import {
CLASS_DEFINE_CHUNK_NAME,
COMMON_CHUNK_NAME,
DEFAULT_LINK_AFTER,
} from '../../../const/generator';
import Scope from '../../../utils/Scope';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IScope,
} from '../../../types';
import { generateCompositeType } from '../../../utils/compositeType';
import { parseExpressionConvertThis2Context } from '../../../utils/expressionParser';
import { isContainerSchema } from '../../../utils/schema';
import { REACT_CHUNK_NAME } from './const';
export interface PluginConfig {
fileType: string;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const scope = Scope.createRootScope();
const dataSourceConfig = isContainerSchema(pre.ir) ? pre.ir.dataSource : null;
const dataSourceItems: InterpretDataSourceConfig[] =
(dataSourceConfig && dataSourceConfig.list) || [];
const dataSourceEngineOptions = { runtimeConfig: true };
if (dataSourceItems.length > 0) {
const requestHandlersMap: Record<string, JSExpression> = {};
dataSourceItems.forEach((ds) => {
const dsType = ds.type || 'fetch';
if (!(dsType in requestHandlersMap) && dsType !== 'custom') {
const handlerFactoryName = `__$$create${changeCase.pascal(dsType)}RequestHandler`;
requestHandlersMap[dsType] = {
type: 'JSExpression',
value:
handlerFactoryName + (dsType === 'urlParams' ? '(window.location.search)' : '()'),
};
const handlerFactoryExportName = `create${changeCase.pascal(dsType)}Handler`;
const handlerPkgName = `@ali/lowcode-datasource-${changeCase.kebab(dsType)}-handler`;
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: `
import { ${handlerFactoryExportName} as ${handlerFactoryName} } from '${handlerPkgName}';
`,
linkAfter: [],
});
}
});
Object.assign(dataSourceEngineOptions, { requestHandlersMap });
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: `
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine/runtime';
`,
linkAfter: [],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsVar,
content: `
_dataSourceConfig = this._defineDataSourceConfig();
_dataSourceEngine = __$$createDataSourceEngine(
this._dataSourceConfig,
this,
${generateCompositeType(dataSourceEngineOptions, scope)}
);
get dataSourceMap() {
return this._dataSourceEngine.dataSourceMap || {};
}
reloadDataSource = async () => {
await this._dataSourceEngine.reloadDataSource();
}
`,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsVar]],
});
next.chunks.unshift({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: REACT_CHUNK_NAME.ClassDidMountContent,
content: `
this._dataSourceEngine.reloadDataSource();
`,
linkAfter: [REACT_CHUNK_NAME.ClassDidMountStart],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsMethod,
content: `
_defineDataSourceConfig() {
const _this = this;
return (${generateCompositeType(
{
...dataSourceConfig,
list: [
...dataSourceItems.map((item) => ({
...item,
isInit: wrapAsFunction(item.isInit, scope),
options: wrapAsFunction(item.options, scope),
})),
],
},
scope,
{
handlers: {
function: (jsFunc) => parseExpressionConvertThis2Context(jsFunc.value, '_this'),
expression: (jsExpr) => parseExpressionConvertThis2Context(jsExpr.value, '_this'),
},
},
)});
}
`,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]],
});
}
return next;
};
return plugin;
};
export default pluginFactory;
function wrapAsFunction(value: CompositeValue, scope: IScope): CompositeValue {
if (isJSExpression(value) || isJSFunction(value)) {
return {
type: 'JSExpression',
value: `function(){ return ((${value.value}))}`,
};
}
return {
type: 'JSExpression',
value: `function(){return((${generateCompositeType(value, scope)}))}`,
};
}

View File

@ -0,0 +1,58 @@
import {
CLASS_DEFINE_CHUNK_NAME,
COMMON_CHUNK_NAME,
DEFAULT_LINK_AFTER,
} from '../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
} from '../../../types';
export interface PluginConfig {
fileType: string;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.InternalDepsImport,
// TODO: 下面这个路径有没有更好的方式来获取?而非写死
content: `
import { i18n as _$$i18n } from '../../i18n';
`,
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsMethod,
content: `
i18n = (i18nKey) => {
return _$$i18n(i18nKey);
}
`,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,93 @@
import {
CLASS_DEFINE_CHUNK_NAME,
COMMON_CHUNK_NAME,
DEFAULT_LINK_AFTER,
} from '../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IContainerInfo,
} from '../../../types';
export interface PluginConfig {
fileType: string;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
next.contextData.useRefApi = true;
const useRef = !!ir.analyzeResult?.isUsingRef;
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.InternalDepsImport,
// TODO: 下面这个路径有没有更好的方式来获取?而非写死
content: `
import utils${useRef ? ', { RefsManager }' : ''} from '../../utils';
`,
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.ConstructorContent,
content: 'this.utils = utils;',
linkAfter: [CLASS_DEFINE_CHUNK_NAME.ConstructorStart],
});
if (useRef) {
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.ConstructorContent,
content: 'this._refsManager = new RefsManager();',
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.ConstructorContent]],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsMethod,
content: `
$ = (refName) => {
return this._refsManager.get(refName);
}
`,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsMethod,
content: `
$$ = (refName) => {
return this._refsManager.getAll(refName);
}
`,
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]],
});
}
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,108 @@
import { CLASS_DEFINE_CHUNK_NAME, DEFAULT_LINK_AFTER } from '../../../const/generator';
import { REACT_CHUNK_NAME } from './const';
import { generateFunction } from '../../../utils/jsExpression';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeChunk,
ICodeStruct,
IContainerInfo,
} from '../../../types';
import { isJSFunction, isJSExpression } from '@ali/lowcode-types';
export interface PluginConfig {
fileType: string;
exportNameMapping: Record<string, string>;
normalizeNameMapping: Record<string, string>;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
exportNameMapping: {},
normalizeNameMapping: {},
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
if (ir.lifeCycles) {
const { lifeCycles } = ir;
const chunks = Object.keys(lifeCycles).map<ICodeChunk | null>((lifeCycleName) => {
// 过滤掉非法数据(有些场景下会误传入空字符串或 null)
if (
!isJSFunction(lifeCycles[lifeCycleName]) &&
!isJSExpression(lifeCycles[lifeCycleName])
) {
return null;
}
let normalizeName;
// constructor会取到对象的构造函数
if (lifeCycleName === 'constructor') {
normalizeName = lifeCycleName;
} else {
normalizeName = cfg.normalizeNameMapping[lifeCycleName] || lifeCycleName;
}
const exportName = cfg.exportNameMapping[lifeCycleName] || lifeCycleName;
if (normalizeName === 'constructor') {
return {
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.ConstructorContent,
content: generateFunction(lifeCycles[lifeCycleName], { isBlock: true }),
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.ConstructorStart]],
};
}
if (normalizeName === 'componentDidMount') {
return {
type: ChunkType.STRING,
fileType: cfg.fileType,
name: REACT_CHUNK_NAME.ClassDidMountContent,
content: generateFunction(lifeCycles[lifeCycleName], { isBlock: true }),
linkAfter: [REACT_CHUNK_NAME.ClassDidMountStart],
};
}
if (normalizeName === 'render') {
return {
type: ChunkType.STRING,
fileType: cfg.fileType,
name: REACT_CHUNK_NAME.ClassRenderPre,
content: generateFunction(lifeCycles[lifeCycleName], { isBlock: true }),
linkAfter: [REACT_CHUNK_NAME.ClassRenderStart],
};
}
return {
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsMethod,
content: generateFunction(lifeCycles[lifeCycleName], {
name: exportName,
isMember: true,
}),
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]],
};
});
next.chunks.push(...chunks.filter((x): x is ICodeChunk => x !== null));
}
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,50 @@
import { CLASS_DEFINE_CHUNK_NAME, DEFAULT_LINK_AFTER } from '../../../const/generator';
import { generateFunction } from '../../../utils/jsExpression';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeChunk,
ICodeStruct,
IContainerInfo,
} from '../../../types';
export interface PluginConfig {
fileType: string;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.JSX,
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
if (ir.methods) {
const { methods } = ir;
const chunks = Object.keys(methods).map<ICodeChunk>((methodName) => ({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: CLASS_DEFINE_CHUNK_NAME.InsMethod,
content: generateFunction(methods[methodName], { name: methodName, isMember: true }),
linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]],
}));
next.chunks.push(...chunks);
}
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,95 @@
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IContainerInfo,
IScope,
NodeGeneratorConfig,
PIECE_TYPE,
} from '../../../types';
import { REACT_CHUNK_NAME } from './const';
import { COMMON_CHUNK_NAME } from '../../../const/generator';
import { createReactNodeGenerator } from '../../../utils/nodeToJSX';
import Scope from '../../../utils/Scope';
export interface PluginConfig {
fileType?: string;
nodeTypeMapping?: Record<string, string>;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg = {
fileType: FileType.JSX,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
nodeTypeMapping: {} as Record<string, string>,
...config,
};
const { nodeTypeMapping } = cfg;
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const generatorPlugins: NodeGeneratorConfig = {
tagMapping: (v) => nodeTypeMapping[v] || v,
};
if (next.contextData.useRefApi) {
generatorPlugins.attrPlugins = [
(attrData, scope, pluginCfg, nextFunc) => {
if (attrData.attrName === 'ref') {
return [
{
name: attrData.attrName,
value: `this._refsManager.linkRef('${attrData.attrValue}')`,
type: PIECE_TYPE.ATTR,
},
];
}
return nextFunc ? nextFunc(attrData, scope, pluginCfg) : [];
},
];
}
const generator = createReactNodeGenerator(generatorPlugins);
const ir = next.ir as IContainerInfo;
const scope: IScope = Scope.createRootScope();
const jsxContent = generator(ir, scope);
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: REACT_CHUNK_NAME.ClassRenderJSX,
content: `
const __$$context = this;
return ${jsxContent};
`,
linkAfter: [REACT_CHUNK_NAME.ClassRenderStart, REACT_CHUNK_NAME.ClassRenderPre],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.CustomContent,
content: `
function __$$createChildContext(oldContext, ext) {
return Object.assign({}, oldContext, ext);
}
`,
linkAfter: [COMMON_CHUNK_NAME.FileExport],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,30 @@
import { COMMON_CHUNK_NAME } from '../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
} from '../../../types';
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
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;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,52 @@
import { COMMON_CHUNK_NAME } from '../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IContainerInfo,
} from '../../../types';
export interface PluginConfig {
fileType: string;
moduleFileType: string;
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
const cfg: PluginConfig = {
fileType: FileType.CSS,
moduleFileType: FileType.JSX,
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IContainerInfo;
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.StyleCssContent,
content: ir.css,
linkAfter: [COMMON_CHUNK_NAME.StyleDepsImport],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.moduleFileType,
name: COMMON_CHUNK_NAME.InternalDepsImport,
content: `import './index.${cfg.fileType}';`,
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,59 @@
import { COMMON_CHUNK_NAME } from '../../const/generator';
import { generateCompositeType } from '../../utils/compositeType';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IProjectInfo,
} from '../../types';
import Scope from '../../utils/Scope';
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IProjectInfo;
const scope = Scope.createRootScope();
const constantStr = generateCompositeType(ir.constants || {}, scope);
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JS,
name: COMMON_CHUNK_NAME.FileVarDefine,
content: `
const __$$constants = (${constantStr});
`,
linkAfter: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JS,
name: COMMON_CHUNK_NAME.FileExport,
content: `
export default __$$constants;
`,
linkAfter: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
COMMON_CHUNK_NAME.FileUtilDefine,
COMMON_CHUNK_NAME.FileMainContent,
],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,17 @@
import template from './template';
import entry from './plugins/entry';
import entryHtml from './plugins/entryHtml';
import globalStyle from './plugins/globalStyle';
import packageJSON from './plugins/packageJSON';
import router from './plugins/router';
export default {
template,
plugins: {
entry,
entryHtml,
globalStyle,
packageJSON,
router,
},
};

View File

@ -0,0 +1,56 @@
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
} from '../../../../../types';
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
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: 'app',
},
router: {
type: 'hash',
},
};
createApp(appConfig);
`,
linkAfter: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
COMMON_CHUNK_NAME.FileUtilDefine,
],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,46 @@
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IProjectInfo,
} from '../../../../../types';
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
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 || 'Ice App'}</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
`,
linkAfter: [],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,56 @@
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IProjectInfo,
} from '../../../../../types';
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
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;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,101 @@
import { PackageJSON } from '@ali/lowcode-types';
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IProjectInfo,
} from '../../../../../types';
interface IIceJsPackageJSON extends PackageJSON {
ideMode: {
name: string;
};
iceworks: {
type: string;
adapter: string;
};
originTemplate: string;
}
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
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',
'@ali/lowcode-datasource-engine': '*',
// TODO: 如何动态获取下面这些依赖?
'@ali/lowcode-datasource-url-params-handler': '*',
'@ali/lowcode-datasource-fetch-handler': '*',
'@ali/lowcode-datasource-mtop-handler': '*',
'@ali/lowcode-datasource-mopen-handler': '*',
'intl-messageformat': '^9.3.6',
},
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',
};
ir.packages.forEach((packageInfo) => {
packageJson.dependencies[packageInfo.package] = packageInfo.version;
});
next.chunks.push({
type: ChunkType.JSON,
fileType: FileType.JSON,
name: COMMON_CHUNK_NAME.FileMainContent,
content: packageJson,
linkAfter: [],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,84 @@
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IRouterInfo,
} from '../../../../../types';
const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
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.ImportAliasDefine,
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.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
COMMON_CHUNK_NAME.FileMainContent,
],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,73 @@
import { ResultFile } from '@ali/lowcode-types';
import { createResultFile } from '../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'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 '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
[],
{
name: 'abc',
ext: 'json',
content: `
{
"type": "ice-app",
"builder": "@ali/builder-ice-app"
}
`,
},
];
}

View File

@ -0,0 +1,33 @@
import { ResultFile } from '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
[],
{
name: 'build',
ext: 'json',
content: `
{
"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"
]
}
`,
},
];
}

View File

@ -0,0 +1,25 @@
import { ResultFile } from '@ali/lowcode-types';
import { createResultFile } from '../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'.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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'.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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'.eslintrc',
'js',
`
const { eslint } = require('@ice/spec');
module.exports = eslint;
`,
);
return [[], file];
}

View File

@ -0,0 +1,36 @@
import { ResultFile } from '@ali/lowcode-types';
import { createResultFile } from '../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'.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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'.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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'.prettierrc',
'js',
`
const { prettier } = require('@ice/spec');
module.exports = prettier;
`,
);
return [[], file];
}

View File

@ -0,0 +1,25 @@
import { ResultFile } from '@ali/lowcode-types';
import { createResultFile } from '../../../../../../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'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 '@ali/lowcode-types';
import { createResultFile } from '../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'.stylelintignore',
'',
`
#
build/
tests/
demo/
# node
coverage/
`,
);
return [[], file];
}

View File

@ -0,0 +1,16 @@
import { ResultFile } from '@ali/lowcode-types';
import { createResultFile } from '../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'.stylelintrc',
'js',
`
const { stylelint } = require('@ice/spec');
module.exports = stylelint;
`,
);
return [[], file];
}

View File

@ -0,0 +1,46 @@
import { ResultFile } from '@ali/lowcode-types';
import { createResultFile } from '../../../../../../utils/resultHelper';
export default function getFile(): [string[], ResultFile] {
const file = createResultFile(
'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,52 @@
import { ResultDir } from '@ali/lowcode-types';
import { IProjectTemplate } from '../../../../../types';
import { generateStaticFiles } from './static-files';
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(): ResultDir {
return generateStaticFiles();
},
};
export default icejsTemplate;

View File

@ -0,0 +1,50 @@
import { ResultDir } from '@ali/lowcode-types';
import { createResultDir } from '../../../../../utils/resultHelper';
import { runFileGenerator } from '../../../../../utils/templateHelper';
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.style';
import file18 from './files/src/layouts/BasicLayout/components/Logo/index.jsx';
import file19 from './files/src/layouts/BasicLayout/components/Logo/index.style';
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';
export function generateStaticFiles(root = createResultDir('.')): 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;
}

View File

@ -0,0 +1,19 @@
import template from './template';
import entry from './plugins/entry';
import appConfig from './plugins/appConfig';
import buildConfig from './plugins/buildConfig';
import entryDocument from './plugins/entryDocument';
import globalStyle from './plugins/globalStyle';
import packageJSON from './plugins/packageJSON';
export default {
template,
plugins: {
appConfig,
buildConfig,
entry,
entryDocument,
globalStyle,
packageJSON,
},
};

View File

@ -0,0 +1,50 @@
import changeCase from 'change-case';
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IParseResult,
} from '../../../../../types';
import { ensureValidClassName } from '../../../../../utils/validate';
import { RaxFrameworkOptions } from '../types/RaxFrameworkOptions';
const pluginFactory: BuilderComponentPluginFactory<RaxFrameworkOptions> = (cfg) => {
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IParseResult;
const routes = ir.globalRouter?.routes?.map((route) => ({
path: route.path,
source: `pages/${ensureValidClassName(changeCase.pascalCase(route.fileName))}/index`,
})) || [{ path: '/', source: 'pages/Home/index' }];
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSON,
name: COMMON_CHUNK_NAME.CustomContent,
content: `
{
"routes": ${JSON.stringify(routes, null, 2)},
"window": {
"title": ${JSON.stringify(
cfg?.title || ir.project?.meta?.title || ir.project?.meta?.name || '',
)}
}
}
`,
linkAfter: [],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,51 @@
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IParseResult,
} from '../../../../../types';
import type { RaxFrameworkOptions } from '../types/RaxFrameworkOptions';
const pluginFactory: BuilderComponentPluginFactory<RaxFrameworkOptions> = (cfg) => {
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IParseResult;
const miniAppBuildType =
cfg?.buildConfig?.miniAppBuildType || ir.project?.config?.miniAppBuildType;
const targets = cfg?.targets || ['web'];
const buildCfg = {
inlineStyle: false,
plugins: [],
targets,
miniapp: miniAppBuildType
? {
buildType: miniAppBuildType,
...cfg?.buildConfig?.miniapp,
}
: cfg?.buildConfig?.miniapp,
...cfg?.buildConfig,
};
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSON,
name: COMMON_CHUNK_NAME.CustomContent,
content: `${JSON.stringify(buildCfg, null, 2)}\n`,
linkAfter: [],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,61 @@
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
} from '../../../../../types';
import { RaxFrameworkOptions } from '../types/RaxFrameworkOptions';
const pluginFactory: BuilderComponentPluginFactory<RaxFrameworkOptions> = (cfg) => {
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JS,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: `
import { runApp } from 'rax-app';
import './global.${cfg?.globalStylesFileType || 'css'}';
`,
linkAfter: [],
});
// 应用配置
const appConfig = cfg?.appConfig || {};
Object.assign(appConfig, {
// 路由配置
router: {
mode: 'hash',
...appConfig.router,
},
});
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JS,
name: COMMON_CHUNK_NAME.FileMainContent,
content: `
runApp(${JSON.stringify(appConfig, null, 2)});
`,
linkAfter: [
COMMON_CHUNK_NAME.ExternalDepsImport,
COMMON_CHUNK_NAME.InternalDepsImport,
COMMON_CHUNK_NAME.ImportAliasDefine,
COMMON_CHUNK_NAME.FileVarDefine,
COMMON_CHUNK_NAME.FileUtilDefine,
],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,63 @@
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IProjectInfo,
} from '../../../../../types';
import { RaxFrameworkOptions } from '../types/RaxFrameworkOptions';
/**
* 使
*/
const pluginFactory: BuilderComponentPluginFactory<RaxFrameworkOptions> = (cfg) => {
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IProjectInfo;
next.chunks.push({
type: ChunkType.STRING,
fileType: FileType.JSX,
name: COMMON_CHUNK_NAME.CustomContent,
content: `
import { createElement } from 'rax';
import { Root, Style, Script } from 'rax-document';
function Document() {
return (
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,viewport-fit=cover"
/>
<title>${cfg?.title || ir?.meta?.name || 'Rax App'}</title>
<Style />
</head>
<body>
{/* root container */}
<Root />
<Script />
</body>
</html>
);
}
export default Document;
`,
linkAfter: [],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,64 @@
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IProjectInfo,
} from '../../../../../types';
export interface GlobalStylePluginConfig {
fileType: string;
}
const pluginFactory: BuilderComponentPluginFactory<GlobalStylePluginConfig> = (
config?: Partial<GlobalStylePluginConfig>,
) => {
const cfg: GlobalStylePluginConfig = {
fileType: FileType.SCSS,
...config,
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IProjectInfo;
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.StyleDepsImport,
content: '',
linkAfter: [],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.StyleCssContent,
content: `
body {
-webkit-font-smoothing: antialiased;
}
`,
linkAfter: [COMMON_CHUNK_NAME.StyleDepsImport],
});
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.StyleCssContent,
content: ir.css || '',
linkAfter: [COMMON_CHUNK_NAME.StyleDepsImport],
});
return next;
};
return plugin;
};
export default pluginFactory;

View File

@ -0,0 +1,131 @@
import changeCase from 'change-case';
import { NpmInfo, PackageJSON } from '@ali/lowcode-types';
import { COMMON_CHUNK_NAME } from '../../../../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
FileType,
ICodeStruct,
IProjectInfo,
} from '../../../../../types';
import { isNpmInfo } from '../../../../../utils/schema';
import { getErrorMessage } from '../../../../../utils/errors';
import { calcCompatibleVersion } from '../../../../../utils/version';
import { RaxFrameworkOptions } from '../types/RaxFrameworkOptions';
const pluginFactory: BuilderComponentPluginFactory<RaxFrameworkOptions> = (cfg) => {
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IProjectInfo;
const npmDeps = getNpmDependencies(ir);
const packageJson: PackageJSON = {
name: cfg?.packageName || '@ali/rax-app-demo',
private: true,
version: cfg?.packageVersion || '1.0.0',
scripts: {
start: 'rax-app start',
build: 'rax-app build',
eslint: 'eslint --ext .js,.jsx ./',
stylelint: 'stylelint "**/*.{css,scss,less}"',
prettier: 'prettier **/* --write',
lint: 'npm run eslint && npm run stylelint',
},
dependencies: {
// 数据源相关的依赖:
[cfg?.datasourceConfig?.enginePackage || '@ali/lowcode-datasource-engine']:
cfg?.datasourceConfig?.engineVersion || 'latest',
// TODO: [p1] 如何动态获取下究竟用了哪些类型的数据源?
...['url-params', 'fetch', 'mtop', 'mopen'].reduce(
(acc, dsType) => ({
...acc,
[cfg?.datasourceConfig?.handlersPackages?.[dsType] ||
`@ali/lowcode-datasource-${changeCase.kebab(dsType)}-handler`]:
cfg?.datasourceConfig?.handlersVersion?.[dsType] || 'latest',
}),
{},
),
// 环境判断
'universal-env': '^3.2.0',
// 国际化相关依赖:
'intl-messageformat': '^9.3.6',
// 基础库
rax: '^1.1.0',
'rax-document': '^0.1.6',
// 其他组件库
...npmDeps.reduce<Record<string, string>>(
(acc, npm) => ({
...acc,
[npm.package]: npm.version || '*',
}),
{},
),
},
devDependencies: {
'@iceworks/spec': '^1.0.0',
'rax-app': '^3.0.0',
eslint: '^6.8.0',
prettier: '^2.1.2',
stylelint: '^13.7.2',
},
};
next.chunks.push({
type: ChunkType.JSON,
fileType: FileType.JSON,
name: COMMON_CHUNK_NAME.FileMainContent,
content: packageJson,
linkAfter: [],
});
return next;
};
return plugin;
};
export default pluginFactory;
function getNpmDependencies(project: IProjectInfo): NpmInfo[] {
const npmDeps: NpmInfo[] = [];
const npmNameToPkgMap = new Map<string, NpmInfo>();
const allDeps = project.packages;
allDeps.forEach((dep) => {
if (!isNpmInfo(dep)) {
return;
}
const existing = npmNameToPkgMap.get(dep.package);
if (!existing) {
npmNameToPkgMap.set(dep.package, dep);
npmDeps.push(dep);
return;
}
if (existing.version !== dep.version) {
try {
npmNameToPkgMap.set(dep.package, {
...existing,
version: calcCompatibleVersion(existing.version, dep.version),
});
} catch (e) {
throw new Error(
`Cannot find compatible version for ${dep.package}. Detail: ${getErrorMessage(e)}`,
);
}
}
});
return npmDeps;
}

View File

@ -0,0 +1,15 @@
/* eslint-disable max-len */
/* Note: this file is generated by "npm run template", please dont modify this file directly */
/* -- instead, you should modify "static-files/rax/.eslintignore.template" and run "npm run template" */
import { ResultFile } from '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
['.'],
{
name: '.eslintignore',
ext: '',
content: 'node_modules/\nlib/\ndist/\nbuild/\ncoverage/\ndemo/\nes/\n.rax/\n',
},
];
}

View File

@ -0,0 +1,16 @@
/* eslint-disable max-len */
/* Note: this file is generated by "npm run template", please dont modify this file directly */
/* -- instead, you should modify "static-files/rax/.eslintrc.js.template" and run "npm run template" */
import { ResultFile } from '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
['.'],
{
name: '.eslintrc',
ext: 'js',
content:
"const { getESLintConfig } = require('@iceworks/spec');\n\n// https://www.npmjs.com/package/@iceworks/spec\nmodule.exports = {\n ...getESLintConfig('rax'),\n rules: {\n 'max-len': ['error', { code: 200 }],\n 'function-paren-newline': 'off',\n '@typescript-eslint/indent': 'off',\n 'prettier/prettier': 'off',\n 'no-empty': 'off',\n 'no-unused-vars': 'off',\n '@iceworks/best-practices/recommend-functional-component': 'off',\n },\n};\n",
},
];
}

View File

@ -0,0 +1,16 @@
/* eslint-disable max-len */
/* Note: this file is generated by "npm run template", please dont modify this file directly */
/* -- instead, you should modify "static-files/rax/.gitignore.template" and run "npm run template" */
import { ResultFile } from '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
['.'],
{
name: '.gitignore',
ext: '',
content:
'# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n*~\n*.swp\n*.log\n\n.DS_Store\n.idea/\n.temp/\n\nbuild/\ndist/\nlib/\ncoverage/\nnode_modules/\n.rax/\n\ntemplate.yml',
},
];
}

View File

@ -0,0 +1,15 @@
/* eslint-disable max-len */
/* Note: this file is generated by "npm run template", please dont modify this file directly */
/* -- instead, you should modify "static-files/rax/.prettierignore.template" and run "npm run template" */
import { ResultFile } from '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
['.'],
{
name: '.prettierignore',
ext: '',
content: 'node_modules/\nlib/\ndist/\nbuild/\ncoverage/\ndemo/\nes/\n.rax/\n',
},
];
}

View File

@ -0,0 +1,16 @@
/* eslint-disable max-len */
/* Note: this file is generated by "npm run template", please dont modify this file directly */
/* -- instead, you should modify "static-files/rax/.prettierrc.js.template" and run "npm run template" */
import { ResultFile } from '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
['.'],
{
name: '.prettierrc',
ext: 'js',
content:
"const { getPrettierConfig } = require('@iceworks/spec');\n\nmodule.exports = getPrettierConfig('rax');\n",
},
];
}

View File

@ -0,0 +1,15 @@
/* eslint-disable max-len */
/* Note: this file is generated by "npm run template", please dont modify this file directly */
/* -- instead, you should modify "static-files/rax/.stylelintignore.template" and run "npm run template" */
import { ResultFile } from '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
['.'],
{
name: '.stylelintignore',
ext: '',
content: 'node_modules/\nlib/\ndist/\nbuild/\ncoverage/\ndemo/\nes/\n.rax/\n',
},
];
}

View File

@ -0,0 +1,16 @@
/* eslint-disable max-len */
/* Note: this file is generated by "npm run template", please dont modify this file directly */
/* -- instead, you should modify "static-files/rax/.stylelintrc.js.template" and run "npm run template" */
import { ResultFile } from '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
['.'],
{
name: '.stylelintrc',
ext: 'js',
content:
"const { getStylelintConfig } = require('@iceworks/spec');\n\nmodule.exports = getStylelintConfig('rax');\n",
},
];
}

View File

@ -0,0 +1,16 @@
/* eslint-disable max-len */
/* Note: this file is generated by "npm run template", please dont modify this file directly */
/* -- instead, you should modify "static-files/rax/README.md.template" and run "npm run template" */
import { ResultFile } from '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
['.'],
{
name: 'README',
ext: 'md',
content:
'# rax-materials-basic-app\n\n## Getting Started\n\n### `npm run start`\n\nRuns the app in development mode.\n\nOpen [http://localhost:3333](http://localhost:3333) to view it in the browser.\n\nThe page will reload if you make edits.\n\n### `npm run build`\n\nBuilds the app for production to the `build` folder.\n',
},
];
}

View File

@ -0,0 +1,16 @@
/* eslint-disable max-len */
/* Note: this file is generated by "npm run template", please dont modify this file directly */
/* -- instead, you should modify "static-files/rax/jsconfig.json.template" and run "npm run template" */
import { ResultFile } from '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
['.'],
{
name: 'jsconfig',
ext: 'json',
content:
'{\n "compilerOptions": {\n "baseUrl": ".",\n "jsx": "react",\n "paths": {\n "@/*": ["./src/*"],\n "rax-app": [".rax/index.ts"]\n }\n }\n}\n',
},
];
}

View File

@ -0,0 +1,16 @@
/* eslint-disable max-len */
/* Note: this file is generated by "npm run template", please dont modify this file directly */
/* -- instead, you should modify "static-files/rax/tsconfig.json.template" and run "npm run template" */
import { ResultFile } from '@ali/lowcode-types';
export default function getFile(): [string[], ResultFile] {
return [
['.'],
{
name: 'tsconfig',
ext: 'json',
content:
'{\n "compileOnSave": false,\n "buildOnSave": false,\n "compilerOptions": {\n "baseUrl": ".",\n "outDir": "build",\n "module": "esnext",\n "target": "es6",\n "jsx": "preserve",\n "jsxFactory": "createElement",\n "moduleResolution": "node",\n "allowSyntheticDefaultImports": true,\n "lib": ["es6", "dom"],\n "sourceMap": true,\n "allowJs": true,\n "rootDir": "./",\n "forceConsistentCasingInFileNames": true,\n "noImplicitReturns": true,\n "noImplicitThis": true,\n "noImplicitAny": false,\n "importHelpers": true,\n "strictNullChecks": true,\n "suppressImplicitAnyIndexErrors": true,\n "noUnusedLocals": true,\n "skipLibCheck": true,\n "paths": {\n "@/*": ["./src/*"],\n "rax-app": [".rax/index.ts"]\n }\n },\n "include": ["src", ".rax"],\n "exclude": ["node_modules", "build", "public"]\n}',
},
];
}

View File

@ -0,0 +1,60 @@
import { ResultDir } from '@ali/lowcode-types';
import { IProjectTemplate } from '../../../../../types';
import { generateStaticFiles } from './static-files';
const raxAppTemplate: IProjectTemplate = {
slots: {
components: {
path: ['src', 'components'],
},
pages: {
path: ['src', 'pages'],
},
router: {
path: ['src'],
fileName: 'router',
},
entry: {
path: ['src'],
fileName: 'app',
},
appConfig: {
path: ['src'],
fileName: 'app',
},
buildConfig: {
path: [],
fileName: 'build',
},
constants: {
path: ['src'],
fileName: 'constants',
},
utils: {
path: ['src'],
fileName: 'utils',
},
i18n: {
path: ['src'],
fileName: 'i18n',
},
globalStyle: {
path: ['src'],
fileName: 'global',
},
htmlEntry: {
path: ['src', 'document'],
fileName: 'index',
},
packageJSON: {
path: [],
fileName: 'package',
},
},
async generateTemplate(): Promise<ResultDir> {
return generateStaticFiles();
},
};
export default raxAppTemplate;

View File

@ -0,0 +1,29 @@
/* Note: this file is generated by "npm run template", please dont modify this file directly */
import { ResultDir } from '@ali/lowcode-types';
import { createResultDir } from '../../../../../utils/resultHelper';
import { runFileGenerator } from '../../../../../utils/templateHelper';
import file0 from './files/.eslintignore';
import file1 from './files/.eslintrc.js';
import file2 from './files/.gitignore';
import file3 from './files/.prettierignore';
import file4 from './files/.prettierrc.js';
import file5 from './files/.stylelintignore';
import file6 from './files/.stylelintrc.js';
import file7 from './files/jsconfig.json';
import file8 from './files/README.md';
import file9 from './files/tsconfig.json';
export function generateStaticFiles(root = createResultDir('.')): ResultDir {
runFileGenerator(root, file0);
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);
return root;
}

Some files were not shown because too many files have changed in this diff Show More