chore: import upstream snapshot with attribution
Deploy Github Pages / deploy (push) Failing after 1s
Deploy Github Pages / deploy (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": ["eslint-config-ali/typescript/react", "prettier"],
|
||||
"ignorePatterns": ["**/dist/**/*", "**/lib/**/*", "**/node_modules/**/*", "scripts/**/*"],
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-definitions": "off",
|
||||
"@typescript-eslint/dot-notation": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"import/no-cycle": "off",
|
||||
"no-nested-ternary": "off",
|
||||
"no-useless-return": "off",
|
||||
"no-param-reassign": "off",
|
||||
"prefer-destructuring": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
name: CI
|
||||
# Event设置为main分支的pull request事件,
|
||||
# 这里的main分支相当于master分支,github项目新建是把main设置为默认分支,我懒得改了所以就保持这样吧
|
||||
on:
|
||||
pull_request:
|
||||
branches: main
|
||||
jobs:
|
||||
# 只需要定义一个job并命名为CI
|
||||
CI:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# 拉取项目代码
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
# 给当前环境下载node
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
# 检查缓存
|
||||
# 如果key命中缓存则直接将缓存的文件还原到 path 目录,从而减少流水线运行时间
|
||||
# 若 key 没命中缓存时,在当前Job成功完成时将自动创建一个新缓存
|
||||
- name: Cache
|
||||
# 缓存命中结果会存储在steps.[id].outputs.cache-hit里,该变量在继后的step中可读
|
||||
id: cache-dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
# 缓存文件目录的路径
|
||||
path: |
|
||||
**/node_modules
|
||||
# key中定义缓存标志位的生成方式。runner.OS指当前环境的系统。外加对yarn.lock内容生成哈希码作为key值,如果yarn.lock改变则代表依赖有变化。
|
||||
# 这里用yarn.lock而不是package.json是因为package.json中还有version和description之类的描述项目但和依赖无关的属性
|
||||
key: ${{runner.OS}}-${{hashFiles('**/yarn.lock')}}
|
||||
# 安装依赖
|
||||
- name: Installing Dependencies
|
||||
# 如果缓存标志位没命中,则执行该step。否则就跳过该step
|
||||
if: steps.cache-dependencies.outputs.cache-hit != 'true'
|
||||
run: yarn install
|
||||
# 预构建
|
||||
- name: Prebuild
|
||||
run: yarn build
|
||||
# 运行代码扫描
|
||||
- name: Running Lint
|
||||
# 通过前面章节定义的命令行执行代码扫描
|
||||
run: yarn eslint
|
||||
# 运行自动化测试
|
||||
- name: Running Test
|
||||
# 通过前面章节定义的命令行执行自动化测试
|
||||
run: yarn test
|
||||
@@ -0,0 +1,53 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy Github Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ['main']
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: 'pages'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Build docs
|
||||
run: yarn typedoc
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
# Upload dist repository
|
||||
path: './docs'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
node_modules/
|
||||
dist/
|
||||
lib/
|
||||
coverage/
|
||||
|
||||
/docs
|
||||
|
||||
.umi/
|
||||
.umi-production/
|
||||
.log
|
||||
.eslintcache
|
||||
*.pem
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn commitlint --edit "$1"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged --allow-empty
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 NetEase
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,125 @@
|
||||
<p align="center">
|
||||
<img width="200" src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30218210645/b186/3974/338b/2ddfa3cd042cf988ca452686552f8462.png" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Tango LowCode Builder</h1>
|
||||
<div align="center">
|
||||
|
||||
A source code based low-code builder.
|
||||
|
||||
[](https://github.com/NetEase/tango/blob/main/LICENSE)
|
||||
[](http://npmjs.org/package/@music163/tango-designer)
|
||||
|
||||
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108735057/7ba9/dced/9ac3/420f6e04b371dd47de06e7d71142560d.gif" alt="preview" />
|
||||
|
||||
</div>
|
||||
|
||||
English | [简体中文](/README.zh-CN.md)
|
||||
|
||||
## 📄 Documentation
|
||||
|
||||
You can view the detailed usage guide through the following links:
|
||||
|
||||
- Document site: <https://netease.github.io/tango-site/>
|
||||
- Playground application: <https://tango-demo.musicfe.com/designer/>
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- Tested in the production environment of NetEase Cloud Music, can be flexibly integrated into low-code platforms, local development tools, etc.
|
||||
- Based on source code AST, with no private DSL and protocol
|
||||
Real-time code generation capability, supporting source code in and source code out
|
||||
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components
|
||||
- Developed using TypeScript, providing complete type definition files
|
||||
|
||||
## 💡 Examples
|
||||
|
||||
You can use tango to build lowcode builders, for example:
|
||||
|
||||
| Preview | Description |
|
||||
| ---------------------------------------------------------------------------------------------- | ----------------------- |
|
||||
|  | Internal App Builder |
|
||||
|  | Dashboard App Builder |
|
||||
|  | Mobile App Builder |
|
||||
|  | ReactNative App Builder |
|
||||
|  | Email Builder |
|
||||
|
||||
## 🌐 Compatibility
|
||||
|
||||
- Modern browsers(Chrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions)
|
||||
|
||||
## 💻 Development
|
||||
|
||||
### Environment
|
||||
|
||||
- Node `>= 18`
|
||||
- Yarn `>= 1.22 && < 2`
|
||||
|
||||
### Development Quick Start
|
||||
|
||||
```bash
|
||||
# clone the repo
|
||||
git clone https://github.com/NetEase/tango.git
|
||||
|
||||
# enter the project root
|
||||
cd tango
|
||||
|
||||
# install dependencies
|
||||
yarn
|
||||
|
||||
# start the designer playground app
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Local Https certificate
|
||||
|
||||
If you need to use https in the local development environment, you can use the following command to generate a certificate:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# install the local certificate
|
||||
mkcert -install
|
||||
|
||||
# enter the playground app directory
|
||||
cd apps/playground
|
||||
|
||||
# generate a certificate
|
||||
mkcert local.netease.com # or change to your own domain name
|
||||
```
|
||||
|
||||
## 💬 Community
|
||||
|
||||
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
|
||||
|
||||
- Discord: <https://discord.gg/B6hkGTe4Rz>
|
||||
- [Usage Trends](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Please read the [github contribution guide](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) first。
|
||||
|
||||
- Clone the repository
|
||||
- Create a branch
|
||||
- Commit and push your code
|
||||
- Open a Pull Request
|
||||
|
||||
## 💗 Acknowledgments
|
||||
|
||||
Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the developers who participated in the Tango project.
|
||||
|
||||
Thank you to CodeSandbox for providing the [Sandpack](https://sandpack.codesandbox.io/) project, which provides powerful online code execution capabilities for Tango.
|
||||
|
||||
## 📣 Product Promotion
|
||||
|
||||

|
||||
|
||||
Don't waste your time restoring the UI, try the NetEase Cloud Music "Seal D2C" development tool! Easily turn your design into code, support React, RN, Vue, WeChat apps and other multi-end scenarios, what you see is what you get!
|
||||
|
||||
Experience "Seal D2C" now:
|
||||
|
||||
- I'm a Figma user: <https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
|
||||
- I'm a MasterGo user: <https://mastergo.com/community/plugin/98956774428196/>
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the terms of the [MIT license](./LICENSE)
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`NetEase/tango`
|
||||
- 原始仓库:https://github.com/NetEase/tango
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
<p align="center">
|
||||
<img width="200" src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30218210645/b186/3974/338b/2ddfa3cd042cf988ca452686552f8462.png" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Tango 低代码设计器</h1>
|
||||
<div align="center">
|
||||
|
||||
一个源码驱动的低代码设计器框架
|
||||
|
||||
[](https://github.com/NetEase/tango/blob/main/LICENSE)
|
||||
[](http://npmjs.org/package/@music163/tango-designer)
|
||||
|
||||
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108735057/7ba9/dced/9ac3/420f6e04b371dd47de06e7d71142560d.gif" alt="preview" />
|
||||
|
||||
</div>
|
||||
|
||||
简体中文 | [English](/README.md)
|
||||
|
||||
## 📄 文档
|
||||
|
||||
可以通过下面的链接查看详细的使用指南:
|
||||
|
||||
- 官方文档站点: <https://netease.github.io/tango-site/>
|
||||
- 演示应用: <https://tango-demo.musicfe.com/designer/>
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
|
||||
- 提供基于源码 AST 驱动的低代码引擎,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出,适配多种源码开发场景
|
||||
- 提供开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件和完善的文档支持
|
||||
|
||||
## 💡 例子
|
||||
|
||||
你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
|
||||
|
||||
| 预览图 | 说明 |
|
||||
| ---------------------------------------------------------------------------------------------- | -------------------- |
|
||||
|  | 中后台系统搭建 |
|
||||
|  | 仪表盘应用搭建 |
|
||||
|  | H5活动页面搭建 |
|
||||
|  | ReactNative 应用搭建 |
|
||||
|  | 营销类邮件搭建 |
|
||||
|
||||
## 🌐 兼容环境
|
||||
|
||||
- 现代浏览器(Chrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions)
|
||||
|
||||
## 💻 开发
|
||||
|
||||
### 推荐开发环境
|
||||
|
||||
- Node `>= 18`
|
||||
- Yarn `>= 1.22 && < 2`
|
||||
|
||||
### 本地开发调试方法
|
||||
|
||||
```bash
|
||||
# 下载仓库
|
||||
git clone https://github.com/NetEase/tango.git
|
||||
|
||||
# 进入项目根目录
|
||||
cd tango
|
||||
|
||||
# 安装依赖
|
||||
yarn
|
||||
|
||||
# 启动设计器示例
|
||||
yarn start
|
||||
```
|
||||
|
||||
### 本地 https 证书
|
||||
|
||||
如果需要在本地开发环境中使用 https,可以使用以下命令生成证书:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# 将 mkcert 添加到本地根 CA,仅在本地生效
|
||||
mkcert -install
|
||||
|
||||
# 进入 playground 应用目录
|
||||
cd apps/playground
|
||||
|
||||
# 为网站生成一个由 mkcert 签名的证书
|
||||
mkcert local.netease.com
|
||||
```
|
||||
|
||||
## 💬 社区讨论
|
||||
|
||||
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
|
||||
|
||||
- Discord: <https://discord.gg/B6hkGTe4Rz>
|
||||
- [使用趋势](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
|
||||
|
||||
## 🤝 参与共建
|
||||
|
||||
请先阅读 [贡献指南](https://docs.github.com/en/get-started/quickstart/contributing-to-projects)。
|
||||
|
||||
- 克隆仓库
|
||||
- 创建分支
|
||||
- 提交代码
|
||||
- 合并修改 git rebase master
|
||||
- 发起 Pull Request
|
||||
|
||||
## 💗 致谢
|
||||
|
||||
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的开发者。
|
||||
|
||||
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。
|
||||
|
||||
## 📣 产品推广
|
||||
|
||||

|
||||
|
||||
不要再浪费时间还原 UI 啦,快来试试网易云音乐「海豹 D2C」研发工具吧!轻松将设计稿转为代码,支持 React、RN、Vue、微信小程序等多端场景,所见即所得!
|
||||
|
||||
立即体验「海豹 D2C」:
|
||||
|
||||
- 我是 Figma 用户:<https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
|
||||
- 我是 MasterGo 用户:<https://mastergo.com/community/plugin/98956774428196/>
|
||||
|
||||
## 📄 开源协议
|
||||
|
||||
此项目遵循 [MIT 开源协议](./LICENSE)
|
||||
@@ -0,0 +1,9 @@
|
||||
/node_modules
|
||||
/.env.local
|
||||
/.umirc.local.ts
|
||||
/config/config.local.ts
|
||||
/src/.umi
|
||||
/src/.umi-production
|
||||
/src/.umi-test
|
||||
/dist
|
||||
.swc
|
||||
@@ -0,0 +1,65 @@
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'umi';
|
||||
|
||||
const resolvePackageIndex = (relativeEntry: string) =>
|
||||
path.resolve(__dirname, '../../packages/', relativeEntry);
|
||||
|
||||
export default defineConfig({
|
||||
routes: [
|
||||
{ path: '/', component: 'index' },
|
||||
{ path: '/mail', component: 'mail' },
|
||||
{ path: '/docs', component: 'docs' },
|
||||
],
|
||||
npmClient: 'yarn',
|
||||
mfsu: false,
|
||||
codeSplitting: false,
|
||||
alias: {
|
||||
'@music163/tango-helpers': resolvePackageIndex('helpers/src/index.ts'),
|
||||
'@music163/tango-core': resolvePackageIndex('core/src/index.ts'),
|
||||
'@music163/tango-context': resolvePackageIndex('context/src/index.ts'),
|
||||
'@music163/tango-ui': resolvePackageIndex('ui/src/index.ts'),
|
||||
'@music163/tango-designer': resolvePackageIndex('designer/src/index.ts'),
|
||||
'@music163/tango-sandbox': resolvePackageIndex('sandbox/src/index.ts'),
|
||||
'@music163/tango-setting-form': resolvePackageIndex('setting-form/src/index.ts'),
|
||||
},
|
||||
externals: {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
'styled-components': 'styled',
|
||||
moment: 'moment',
|
||||
antd: 'antd',
|
||||
},
|
||||
headScripts: [
|
||||
'https://unpkg.com/react@18.2.0/umd/react.development.js',
|
||||
'https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js',
|
||||
'https://unpkg.com/react-is@18.2.0/umd/react-is.production.min.js',
|
||||
'https://unpkg.com/moment/min/moment-with-locales.js',
|
||||
'https://unpkg.com/styled-components@5.3.11/dist/styled-components.js',
|
||||
'https://unpkg.com/antd@4.24.13/dist/antd-with-locales.min.js',
|
||||
'https://unpkg.com/prettier@2.6.0/standalone.js',
|
||||
'https://unpkg.com/prettier@2.6.0/parser-babel.js',
|
||||
],
|
||||
https: {
|
||||
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
|
||||
cert: path.resolve(__dirname, 'local.netease.com.pem'),
|
||||
http2: false,
|
||||
},
|
||||
chainWebpack: (config: any) => {
|
||||
// @see https://github.com/graphql/graphql-js/issues/1272#issuecomment-393903706
|
||||
config.module
|
||||
.rule('mjs')
|
||||
.test(/\.mjs$/)
|
||||
.include.add(/node_modules/)
|
||||
.end()
|
||||
.type('javascript/auto');
|
||||
config.module.rule('js').include.add(resolvePackageIndex('context/src'));
|
||||
config.module.rule('js').include.add(resolvePackageIndex('core/src'));
|
||||
config.module.rule('js').include.add(resolvePackageIndex('designer/src'));
|
||||
config.module.rule('js').include.add(resolvePackageIndex('helpers/src'));
|
||||
config.module.rule('js').include.add(resolvePackageIndex('sandbox/src'));
|
||||
config.module.rule('js').include.add(resolvePackageIndex('setting-form/src'));
|
||||
config.module.rule('js').include.add(resolvePackageIndex('ui/src'));
|
||||
return config;
|
||||
},
|
||||
monorepoRedirect: {},
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "playground",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"author": "Wells <ww.sun@outlook.com>",
|
||||
"scripts": {
|
||||
"dev": "cross-env HOST=local.netease.com PORT=8001 umi dev",
|
||||
"build": "umi build",
|
||||
"postinstall": "umi setup",
|
||||
"setup": "umi setup",
|
||||
"start": "npm run dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/antd": "^0.2.4",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5",
|
||||
"umi": "^4.2.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { IApi } from 'umi';
|
||||
|
||||
export default (api: IApi) => {
|
||||
api.addMiddlewares(() => {
|
||||
return (req, res, next) => {
|
||||
res.setHeader('Origin-Agent-Cluster', '?0');
|
||||
next();
|
||||
};
|
||||
});
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export function FooSetter({ value, ...rest }: any) {
|
||||
return <code {...rest}>fooSetter: {value}</code>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './foo-setter';
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Row, Switch } from 'antd';
|
||||
import { observer } from '@music163/tango-context';
|
||||
|
||||
const OtherPanel = observer(({ autoRemove, setAutoRemove }: any) => {
|
||||
const onChange = (checked: boolean) => {
|
||||
setAutoRemove(checked);
|
||||
};
|
||||
return (
|
||||
<Row>
|
||||
<label>自动移除未引用变量:</label>
|
||||
<Switch
|
||||
checkedChildren="开启"
|
||||
unCheckedChildren="关闭"
|
||||
onChange={onChange}
|
||||
checked={autoRemove}
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
|
||||
export default OtherPanel;
|
||||
@@ -0,0 +1,260 @@
|
||||
import React from 'react';
|
||||
import { Box, Group, Text } from 'coral-system';
|
||||
import { Avatar, Space, Switch } from 'antd';
|
||||
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { registerSetter } from '@music163/tango-designer';
|
||||
import type { IVariableTreeNode } from '@music163/tango-helpers';
|
||||
import { FooSetter } from '../components';
|
||||
import { ToggleButton } from '@music163/tango-ui';
|
||||
|
||||
export * from './mock-files';
|
||||
|
||||
export const bootHelperVariables: IVariableTreeNode[] = [
|
||||
{
|
||||
key: '$helpers',
|
||||
title: '工具函数',
|
||||
children: [
|
||||
{
|
||||
title: '设置变量值',
|
||||
key: '() => tango.setStoreValue("variableName", "variableValue")',
|
||||
type: 'function',
|
||||
help: '设置状态值,tango.setStoreValue("variableName", "variableValue")',
|
||||
},
|
||||
{
|
||||
title: '获取变量值',
|
||||
key: '() => tango.getStoreValue("variableName")',
|
||||
type: 'function',
|
||||
help: '获取状态值',
|
||||
},
|
||||
{ title: '打开弹窗', key: '() => tango.openModal("")', type: 'function' },
|
||||
{ title: '关闭弹窗', key: '() => tango.closeModal("")', type: 'function' },
|
||||
{ title: '切换路由', key: '() => tango.navigateTo("/")', type: 'function' },
|
||||
{
|
||||
title: '拷贝到剪贴板',
|
||||
key: '() => tango.copyToClipboard("hello")',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 注册自定义 setter
|
||||
registerSetter({
|
||||
name: 'fooSetter',
|
||||
component: FooSetter,
|
||||
});
|
||||
|
||||
// 平台 Logo
|
||||
export function Logo() {
|
||||
return (
|
||||
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="14px">
|
||||
<ToggleButton
|
||||
shape="text"
|
||||
items={[
|
||||
{ label: '回到首页', key: 'home' },
|
||||
{ label: '文档', key: 'doc' },
|
||||
{ label: '设置', key: 'setting' },
|
||||
]}
|
||||
>
|
||||
<MenuOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// 项目信息
|
||||
export function ProjectDetail() {
|
||||
return (
|
||||
<Box display="flex" alignItems="center" columnGap="l">
|
||||
<Text className="ProjectName" fontSize="18px" fontWeight="bold" truncated>
|
||||
community-test
|
||||
</Text>
|
||||
<Text className="BranchName" as="code" fontSize="14px" truncated>
|
||||
<BranchesOutlined /> feature/list
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarFooter() {
|
||||
return (
|
||||
<Space direction="vertical" align="center">
|
||||
<QuestionCircleOutlined style={{ fontSize: 20 }} />
|
||||
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
interface ActionsProps {
|
||||
defaultChecked?: boolean;
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
onChange?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
// 平台核心行动点
|
||||
export function Actions({ defaultChecked }: ActionsProps) {
|
||||
return (
|
||||
<Group spacingX="8px">
|
||||
<Switch defaultChecked={defaultChecked} checkedChildren="新版" unCheckedChildren="老版" />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export const menuData: any = {
|
||||
// 常用组件
|
||||
common: [
|
||||
{
|
||||
title: '基本',
|
||||
items: [
|
||||
'Button',
|
||||
'ButtonGroup',
|
||||
'ActionList',
|
||||
'Action',
|
||||
'Image',
|
||||
'Text',
|
||||
'MultilineText',
|
||||
'Link',
|
||||
'Title',
|
||||
'Paragraph',
|
||||
'Icon',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '数据和逻辑',
|
||||
items: ['When', 'DataProvider', 'Interval', 'Each'],
|
||||
},
|
||||
{
|
||||
title: '基础布局',
|
||||
items: [
|
||||
'Section',
|
||||
'Columns',
|
||||
'Box',
|
||||
'Divider',
|
||||
'Space',
|
||||
'Tabs',
|
||||
'Toolbar',
|
||||
'Modal',
|
||||
'Drawer',
|
||||
'SnippetButtonGroup',
|
||||
'Snippet2ColumnLayout',
|
||||
'Snippet3ColumnLayout',
|
||||
'SnippetSuccessResult',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '表单表格',
|
||||
items: [
|
||||
'XTable',
|
||||
'XEditableTable',
|
||||
// 'Table',
|
||||
'XForm',
|
||||
'XFormItem',
|
||||
'XStepForms',
|
||||
// 'SearchForm',
|
||||
// 'Form',
|
||||
// 'FormItem',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '常用图表',
|
||||
items: [
|
||||
'ChartContainer',
|
||||
'BarChart',
|
||||
'LineChart',
|
||||
'PieChart',
|
||||
'FunnelChart',
|
||||
'ScatterChart',
|
||||
'RadarChart',
|
||||
'WordCloud',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '导航',
|
||||
items: [
|
||||
'Breadcrumb',
|
||||
'Dropdown',
|
||||
'Menu',
|
||||
// 'PageHeader',
|
||||
'Pagination',
|
||||
'Steps',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '数据录入',
|
||||
items: [
|
||||
'AutoComplete',
|
||||
'Cascader',
|
||||
'Checkbox',
|
||||
'CheckboxGroup',
|
||||
'DatePicker',
|
||||
'DateRangePicker',
|
||||
'WeekPicker',
|
||||
'MonthPicker',
|
||||
'YearPicker',
|
||||
'Input',
|
||||
'InputNumber',
|
||||
'InputKV',
|
||||
'Mentions',
|
||||
'Radio',
|
||||
'RadioGroup',
|
||||
'Rate',
|
||||
'Select',
|
||||
'Slider',
|
||||
'Switch',
|
||||
'Search',
|
||||
'TextArea',
|
||||
'TimePicker',
|
||||
'TimeRangePicker',
|
||||
'Transfer',
|
||||
'TreeSelect',
|
||||
'Upload',
|
||||
'NosUpload',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '数据展示',
|
||||
items: [
|
||||
'Avatar',
|
||||
'Badge',
|
||||
'RibbonBadge',
|
||||
'Calendar',
|
||||
'Card',
|
||||
'Carousel',
|
||||
'Collapse',
|
||||
'Comment',
|
||||
'Descriptions',
|
||||
'Empty',
|
||||
'Image',
|
||||
'List',
|
||||
'Popover',
|
||||
'Statistic',
|
||||
'Table',
|
||||
'Tag',
|
||||
'CheckableTag',
|
||||
'Skeleton',
|
||||
'SkeletonAvatar',
|
||||
'SkeletonButton',
|
||||
'SkeletonInput',
|
||||
'SkeletonImage',
|
||||
'SkeletonNode',
|
||||
'Spin',
|
||||
'Timeline',
|
||||
'Tooltip',
|
||||
'Tree',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '反馈',
|
||||
items: [
|
||||
'Alert',
|
||||
'Drawer',
|
||||
// 'Message',
|
||||
'Modal',
|
||||
'Notification',
|
||||
'Popconfirm',
|
||||
'Progress',
|
||||
'Result',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/tango-mail': '0.2.4',
|
||||
'@music163/tango-boot': '0.2.5',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
'prop-types': '15.7.2',
|
||||
tslib: '2.5.0',
|
||||
},
|
||||
};
|
||||
|
||||
const tangoConfigJson = {
|
||||
designerConfig: {
|
||||
autoGenerateComponentId: true,
|
||||
},
|
||||
packages: {
|
||||
react: {
|
||||
version: '17.0.2',
|
||||
library: 'React',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
|
||||
},
|
||||
'react-dom': {
|
||||
version: '17.0.2',
|
||||
library: 'ReactDOM',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
|
||||
},
|
||||
'react-is': {
|
||||
version: '16.13.1',
|
||||
library: 'ReactIs',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
|
||||
},
|
||||
'@music163/tango-boot': {
|
||||
description: '云音乐低代码运行时框架',
|
||||
version: '0.2.5',
|
||||
library: 'TangoBoot',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||
// resources: ['http://localhost:9001/boot.js'],
|
||||
},
|
||||
'@music163/tango-mail': {
|
||||
description: 'TangoMail 基础物料',
|
||||
version: '0.2.4',
|
||||
library: 'TangoMail',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'],
|
||||
designerResources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/designer.js'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const routesCode = `
|
||||
import Mail from "./pages/mail";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: Mail,
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
`;
|
||||
|
||||
const entryCode = `
|
||||
import { runApp } from '@music163/tango-boot';
|
||||
import routes from './routes';
|
||||
|
||||
runApp({
|
||||
boot: {
|
||||
mountElement: document.querySelector('#root'),
|
||||
qiankun: false,
|
||||
},
|
||||
|
||||
router: {
|
||||
type: 'browser',
|
||||
config: routes,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
const viewHomeCode = `
|
||||
import React from 'react';
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
} from '@music163/tango-mail';
|
||||
|
||||
const WelcomeEmail = () => (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>The sales intelligence platform that helps you uncover qualified leads.</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Text style={paragraph}>Hi wells,</Text>
|
||||
<Text style={paragraph}>
|
||||
Welcome to Koala, the sales intelligence platform that helps you uncover qualified leads
|
||||
and close deals faster.
|
||||
</Text>
|
||||
<Section style={btnContainer}>
|
||||
<Button style={button} href="https://getkoala.com">
|
||||
Get started
|
||||
</Button>
|
||||
</Section>
|
||||
<Text style={paragraph}>
|
||||
Best,
|
||||
The Koala team
|
||||
</Text>
|
||||
<Hr style={hr} />
|
||||
<Text style={footer}>470 Noor Ave STE B #1148, South San Francisco, CA 94080</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
const main = {
|
||||
backgroundColor: '#ffffff',
|
||||
fontFamily:
|
||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
||||
};
|
||||
|
||||
const container = {
|
||||
margin: '0 auto',
|
||||
padding: '20px 0 48px',
|
||||
};
|
||||
|
||||
const paragraph = {
|
||||
fontSize: '16px',
|
||||
lineHeight: '26px',
|
||||
};
|
||||
|
||||
const btnContainer = {
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
const button = {
|
||||
backgroundColor: '#5F51E8',
|
||||
borderRadius: '3px',
|
||||
color: '#fff',
|
||||
fontSize: '16px',
|
||||
textDecoration: 'none',
|
||||
textAlign: 'center',
|
||||
display: 'block',
|
||||
padding: '12px',
|
||||
};
|
||||
|
||||
const hr = {
|
||||
borderColor: '#cccccc',
|
||||
margin: '20px 0',
|
||||
};
|
||||
|
||||
const footer = {
|
||||
color: '#8898aa',
|
||||
fontSize: '12px',
|
||||
};
|
||||
|
||||
export default WelcomeEmail;
|
||||
`;
|
||||
|
||||
export const mailFiles = [
|
||||
{ filename: '/package.json', code: JSON.stringify(packageJson) },
|
||||
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
|
||||
{ filename: '/README.md', code: '# readme' },
|
||||
{ filename: '/src/index.js', code: entryCode },
|
||||
{ filename: '/src/pages/mail.js', code: viewHomeCode },
|
||||
{ filename: '/src/routes.js', code: routesCode },
|
||||
];
|
||||
@@ -0,0 +1,462 @@
|
||||
const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/antd': '0.2.5',
|
||||
'@music163/tango-boot': '0.3.5',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
'prop-types': '15.7.2',
|
||||
tslib: '2.5.0',
|
||||
},
|
||||
};
|
||||
|
||||
const tangoConfigJson = {
|
||||
designerConfig: {
|
||||
autoGenerateComponentId: true,
|
||||
},
|
||||
packages: {
|
||||
react: {
|
||||
version: '17.0.2',
|
||||
library: 'React',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
|
||||
},
|
||||
'react-dom': {
|
||||
version: '17.0.2',
|
||||
library: 'ReactDOM',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
|
||||
},
|
||||
'react-is': {
|
||||
version: '16.13.1',
|
||||
library: 'ReactIs',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
|
||||
},
|
||||
'styled-components': {
|
||||
version: '5.3.5',
|
||||
library: 'styled',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/styled-components@{{version}}/dist/styled-components.min.js'],
|
||||
},
|
||||
moment: {
|
||||
version: '2.29.4',
|
||||
library: 'moment',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
|
||||
},
|
||||
'@music163/tango-boot': {
|
||||
description: '云音乐低代码运行时框架',
|
||||
version: '0.3.5',
|
||||
library: 'TangoBoot',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||
// resources: ['http://localhost:9001/boot.js'],
|
||||
},
|
||||
'@music163/antd': {
|
||||
description: '云音乐低代码中后台应用基础物料',
|
||||
version: '0.2.6',
|
||||
library: 'TangoAntd',
|
||||
type: 'baseDependency',
|
||||
resources: [
|
||||
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
|
||||
// 'http://localhost:9002/designer.js',
|
||||
'https://unpkg.com/antd@4.24.13/dist/antd.css',
|
||||
],
|
||||
designerResources: [
|
||||
'https://unpkg.com/@music163/antd@{{version}}/dist/designer.js',
|
||||
'https://unpkg.com/antd@4.24.13/dist/antd.css',
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const helperCode = `
|
||||
export function registerComponentPrototype(proto) {
|
||||
if (!proto) return;
|
||||
if (!window.localTangoComponentPrototypes) {
|
||||
window.localTangoComponentPrototypes = {};
|
||||
}
|
||||
if (proto.name) {
|
||||
window.localTangoComponentPrototypes[proto.name] = proto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const routesCode = `
|
||||
import Index from "./pages/list";
|
||||
import Detail from "./pages/detail";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: Index
|
||||
},
|
||||
{
|
||||
path: '/detail',
|
||||
exact: true,
|
||||
component: Detail
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
`;
|
||||
|
||||
const storeIndexCode = `
|
||||
export { default as app } from './app';
|
||||
export { default as counter } from './counter';
|
||||
`;
|
||||
|
||||
const entryCode = `
|
||||
import { runApp } from '@music163/tango-boot';
|
||||
import routes from './routes';
|
||||
import './services';
|
||||
import './stores';
|
||||
import './index.less';
|
||||
|
||||
runApp({
|
||||
boot: {
|
||||
mountElement: document.querySelector('#root'),
|
||||
qiankun: false,
|
||||
},
|
||||
|
||||
router: {
|
||||
type: 'browser',
|
||||
config: routes,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
const storeCounter = `
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
const counter = defineStore({
|
||||
// state
|
||||
num: 0,
|
||||
|
||||
// action
|
||||
increment: () => counter.num++,
|
||||
|
||||
decrement: () => {
|
||||
counter.num--;
|
||||
},
|
||||
}, 'counter');
|
||||
|
||||
export default counter;
|
||||
`;
|
||||
|
||||
const viewHomeCode = `
|
||||
import React from "react";
|
||||
import { definePage } from "@music163/tango-boot";
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
Box,
|
||||
Button,
|
||||
Input,
|
||||
FormilyForm,
|
||||
FormilyFormItem,
|
||||
Table,
|
||||
} from "@music163/antd";
|
||||
import { Space } from "@music163/antd";
|
||||
import { LocalButton } from "../components";
|
||||
import { OutButton } from "../components";
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
|
||||
<Section tid="section0">
|
||||
<Box></Box>
|
||||
</Section>
|
||||
<Section tid="section1" title="Section Title">
|
||||
your input: <Input tid="input1" defaultValue="hello" />
|
||||
copy input: <Input value={tango.page.input1?.value} />
|
||||
<Table
|
||||
columns={[
|
||||
{ title: "姓名", dataIndex: "name", key: "name" },
|
||||
{ title: "年龄", dataIndex: "age", key: "age" },
|
||||
{ title: "住址", dataIndex: "address", key: "address" },
|
||||
]}
|
||||
tid="table1"
|
||||
/>
|
||||
</Section>
|
||||
<Section tid="section2">
|
||||
<Space tid="space1">
|
||||
<LocalButton />
|
||||
<OutButton />
|
||||
<Button tid="button1">button</Button>
|
||||
</Space>
|
||||
</Section>
|
||||
<Section title="区块标题" tid="section3">
|
||||
<FormilyForm tid="formilyForm1">
|
||||
<FormilyFormItem name="input1" component="Input" label="表单项" />
|
||||
<FormilyFormItem name="select1" component="Select" label="表单项" />
|
||||
</FormilyForm>
|
||||
</Section>
|
||||
<Section title="原生 DOM" tid="section4">
|
||||
<h1 style={{ ...{ color: "red" }, fontSize: 64 }}>
|
||||
hello world
|
||||
</h1>
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "#f4f4f4",
|
||||
}}
|
||||
>
|
||||
<form onSubmit={tango.stores.app.submitForm} style={{
|
||||
padding: "10px",
|
||||
}}>
|
||||
<div>
|
||||
<label>Username:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
style={{ margin: "5px" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
style={{ margin: "5px" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Role:</label>
|
||||
<select id="role" name="role" style={{ margin: "5px" }}>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">User</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
marginTop: '5px',
|
||||
padding: "3px 10px",
|
||||
backgroundColor: "#007bff",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: "5px",
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Section>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default definePage(App);
|
||||
`;
|
||||
|
||||
export const emptyPageCode = `
|
||||
import React from "react";
|
||||
import { definePage } from "@music163/tango-boot";
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
} from "@music163/antd";
|
||||
|
||||
function App() {
|
||||
return (<Page title="Detail Page">
|
||||
<Section></Section>
|
||||
</Page>)
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
`;
|
||||
|
||||
const componentsButtonCode = `
|
||||
import React from 'react';
|
||||
import { registerComponentPrototype } from '../utils';
|
||||
|
||||
export default function MyButton(props) {
|
||||
return <button {...props}>my button</button>
|
||||
}
|
||||
|
||||
registerComponentPrototype({
|
||||
name: 'LocalButton',
|
||||
title: 'Local Button',
|
||||
exportType: 'namedExport',
|
||||
package: '/src/components',
|
||||
props: [
|
||||
{ name: 'background', title: '背景色', setter: 'colorSetter' },
|
||||
],
|
||||
});
|
||||
`;
|
||||
|
||||
const outButtonCode = `
|
||||
import React from 'react';
|
||||
|
||||
export default function OutButton(props) {
|
||||
return <button {...props}>Out button (from other file)</button>
|
||||
}
|
||||
`;
|
||||
|
||||
const componentsInputCode = `
|
||||
import React from 'react';
|
||||
import { registerComponentPrototype } from '../utils';
|
||||
|
||||
export default function MyInput(props) {
|
||||
return <input {...props} />;
|
||||
}
|
||||
|
||||
registerComponentPrototype({
|
||||
name: 'LocalInput',
|
||||
title: 'Local Input',
|
||||
exportType: 'namedExport',
|
||||
package: '/src/components',
|
||||
props: [
|
||||
{ name: 'color', title: '文本色', setter: 'colorSetter' },
|
||||
],
|
||||
});
|
||||
`;
|
||||
|
||||
const componentsEntryCode = `
|
||||
export { default as LocalButton } from './button';
|
||||
export { default as LocalInput } from './input';
|
||||
export { default as OutButton } from './out-button';
|
||||
`;
|
||||
|
||||
const storeApp = `
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
export default defineStore({
|
||||
title: 'Page Title',
|
||||
|
||||
array: [1, 2, 3],
|
||||
|
||||
test: function() {
|
||||
console.log('test');
|
||||
},
|
||||
|
||||
submitForm: function(event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.target);
|
||||
const username = formData.get("username");
|
||||
const password = formData.get("password");
|
||||
const role = formData.get("role");
|
||||
console.log("Submitted Data:", { username, password, role });
|
||||
}
|
||||
}, 'app');
|
||||
`;
|
||||
|
||||
const serviceCode = `
|
||||
import { defineServices } from '@music163/tango-boot';
|
||||
import './sub';
|
||||
|
||||
export default defineServices({
|
||||
longLongLongLongLongLongLongLongGet: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
|
||||
formatter: res => res.data,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
get: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
|
||||
formatter: res => res.data,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
list: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/anchor-list-normal',
|
||||
},
|
||||
add: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
description: '新增用户'
|
||||
},
|
||||
update: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
description: '更新用户'
|
||||
},
|
||||
delete: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
|
||||
description: '删除用户'
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
const subServiceCode = `
|
||||
import { defineServices } from '@music163/tango-boot';
|
||||
|
||||
export default defineServices({
|
||||
list: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/anchor-list-normal',
|
||||
},
|
||||
}, {
|
||||
namespace: 'sub',
|
||||
});
|
||||
`;
|
||||
|
||||
const lessCode = `
|
||||
body {
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const cssCode = `
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
|
||||
export const sampleFiles = [
|
||||
{ filename: '/package.json', code: JSON.stringify(packageJson) },
|
||||
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
|
||||
{ filename: '/README.md', code: '# readme' },
|
||||
{ filename: '/src/index.less', code: lessCode },
|
||||
{ filename: '/src/style.css', code: cssCode },
|
||||
{ filename: '/src/index.js', code: entryCode },
|
||||
{ filename: '/src/pages/list.js', code: viewHomeCode },
|
||||
{ filename: '/src/pages/detail.js', code: emptyPageCode },
|
||||
{ filename: '/src/components/button.js', code: componentsButtonCode },
|
||||
{ filename: '/src/components/out-button.js', code: outButtonCode },
|
||||
{ filename: '/src/components/input.js', code: componentsInputCode },
|
||||
{ filename: '/src/components/index.js', code: componentsEntryCode },
|
||||
{ filename: '/src/routes.js', code: routesCode },
|
||||
{ filename: '/src/stores/index.js', code: storeIndexCode },
|
||||
{ filename: '/src/stores/app.js', code: storeApp },
|
||||
{ filename: '/src/stores/counter.js', code: storeCounter },
|
||||
{ filename: '/src/services/index.js', code: serviceCode },
|
||||
{ filename: '/src/services/sub.js', code: subServiceCode },
|
||||
{ filename: '/src/utils/index.js', code: helperCode },
|
||||
];
|
||||
|
||||
export const genDefaultPage = (index: number) => ({
|
||||
name: 'new-page',
|
||||
code: `
|
||||
import React from 'react';
|
||||
import tango, { definePage } from '@music163/tango-boot';
|
||||
import { Page, Section } from '@music163/antd';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Page title="空白模板${index}">
|
||||
<Section></Section>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
`,
|
||||
});
|
||||
@@ -0,0 +1,305 @@
|
||||
import * as basePrototypes from '@music163/antd/prototypes';
|
||||
import type { IComponentPrototype, Dict } from '@music163/tango-helpers';
|
||||
|
||||
const sampleBlockCode = `
|
||||
<Section>
|
||||
<Result
|
||||
status="success"
|
||||
title="Successfully Purchased Cloud Server ECS!"
|
||||
subTitle="Order number: 2017182818828182881 Cloud server configuration takes 1-5 minutes, please wait."
|
||||
extra={[
|
||||
<Button type="primary" key="console">
|
||||
Go Console
|
||||
</Button>,
|
||||
<Button key="buy">Buy Again</Button>,
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
`;
|
||||
|
||||
const SnippetSuccessResult: IComponentPrototype = {
|
||||
name: 'SnippetSuccessResult',
|
||||
title: '成功结果',
|
||||
icon: 'icon-tupian',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: sampleBlockCode,
|
||||
relatedImports: ['Section', 'Result', 'Button'],
|
||||
};
|
||||
|
||||
const Snippet2ColumnLayout: IComponentPrototype = {
|
||||
name: 'Snippet2ColumnLayout',
|
||||
title: '两列布局',
|
||||
icon: 'icon-columns',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={6}></Column>
|
||||
<Column colSpan={6}></Column>
|
||||
</Columns>
|
||||
`,
|
||||
relatedImports: ['Columns', 'Column'],
|
||||
};
|
||||
|
||||
const Snippet3ColumnLayout: IComponentPrototype = {
|
||||
name: 'Snippet3ColumnLayout',
|
||||
title: '三列布局',
|
||||
icon: 'icon-column-3',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
</Columns>
|
||||
`,
|
||||
relatedImports: ['Columns', 'Column'],
|
||||
};
|
||||
|
||||
const SnippetButtonGroup: IComponentPrototype = {
|
||||
name: 'SnippetButtonGroup',
|
||||
title: '按钮组',
|
||||
icon: 'icon-anniuzu',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Space>
|
||||
<Button type="primary">按钮1</Button>
|
||||
<Button>按钮2</Button>
|
||||
</Space>
|
||||
`,
|
||||
relatedImports: ['Space', 'Button'],
|
||||
};
|
||||
|
||||
// hack some prototypes
|
||||
basePrototypes['Section'].siblingNames = [
|
||||
'SnippetButtonGroup',
|
||||
'Snippet2ColumnLayout',
|
||||
'Snippet3ColumnLayout',
|
||||
'SnippetSuccessResult',
|
||||
];
|
||||
|
||||
export const nativeDomPrototypes = () => {
|
||||
const doms = [
|
||||
'div',
|
||||
'span',
|
||||
'h1',
|
||||
'h2',
|
||||
'p',
|
||||
'a',
|
||||
'img',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'input',
|
||||
'button',
|
||||
'form',
|
||||
'table',
|
||||
'tr',
|
||||
'td',
|
||||
'header',
|
||||
'footer',
|
||||
'nav',
|
||||
'section',
|
||||
'article',
|
||||
'aside',
|
||||
'main',
|
||||
'video',
|
||||
'audio',
|
||||
'label',
|
||||
'select',
|
||||
'option',
|
||||
'textarea',
|
||||
'iframe',
|
||||
];
|
||||
const componentPrototypes: Dict<IComponentPrototype> = doms.reduce(
|
||||
(acc: Dict<IComponentPrototype>, tag) => {
|
||||
acc[tag] = {
|
||||
name: tag,
|
||||
title: tag,
|
||||
hasChildren: true,
|
||||
package: '',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'style',
|
||||
title: '样式',
|
||||
group: 'style',
|
||||
setter: 'expressionSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
title: '类名',
|
||||
setter: 'classNameSetter',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
title: 'ID',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
title: '点击事件',
|
||||
setter: 'actionSetter',
|
||||
group: 'event',
|
||||
},
|
||||
],
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
return componentPrototypes;
|
||||
};
|
||||
|
||||
// iconfont: https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=2891794
|
||||
const prototypes: Dict<IComponentPrototype> = {
|
||||
...nativeDomPrototypes(),
|
||||
...(basePrototypes as any),
|
||||
SnippetSuccessResult,
|
||||
Snippet2ColumnLayout,
|
||||
Snippet3ColumnLayout,
|
||||
SnippetButtonGroup,
|
||||
Section: {
|
||||
...basePrototypes['Section'],
|
||||
props: [
|
||||
// ...(basePrototypes['Section'].props as any),
|
||||
{
|
||||
name: 'title',
|
||||
title: '标题',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'extra',
|
||||
title: '额外内容',
|
||||
setter: 'renderSetter',
|
||||
options: [
|
||||
{
|
||||
label: '按钮组',
|
||||
value: 'ButtonGroup',
|
||||
renderBody:
|
||||
'<ButtonGroup><Button>button1</Button><Button>button2</Button></ButtonGroup>',
|
||||
relatedImports: ['ButtonGroup', 'Button'],
|
||||
},
|
||||
],
|
||||
template: '(v) => {\n return {{content}};\n}',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
title: '类名',
|
||||
setter: 'classNameSetter',
|
||||
group: 'style',
|
||||
},
|
||||
],
|
||||
},
|
||||
Box: {
|
||||
name: 'Box',
|
||||
title: '盒子',
|
||||
icon: 'icon-mianban',
|
||||
type: 'container',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
siblingNames: ['Box'],
|
||||
props: [
|
||||
{
|
||||
name: 'aaa',
|
||||
title: 'aaa',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'bbb',
|
||||
title: 'bbb',
|
||||
setter: 'textSetter',
|
||||
deprecated: true,
|
||||
},
|
||||
{
|
||||
name: 'ccc',
|
||||
title: 'ccc',
|
||||
setter: 'textSetter',
|
||||
deprecated: true,
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
title: 'd',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
title: '点击事件',
|
||||
setter: 'eventSetter',
|
||||
template: '(e) => {\n {{content}}\n}',
|
||||
tip: '回调参数说明:e 为事件对象',
|
||||
},
|
||||
{
|
||||
name: 'renderFoo',
|
||||
title: 'foo自定义渲染',
|
||||
setter: 'renderSetter',
|
||||
options: [
|
||||
{
|
||||
label: '占位空间',
|
||||
value: 'Box',
|
||||
render: '() => <Box>test</Box>',
|
||||
relatedImports: ['Box'],
|
||||
},
|
||||
{
|
||||
label: '占位文本',
|
||||
value: 'Text',
|
||||
render: '() => <Text>text</Text>',
|
||||
relatedImports: ['Text'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
title: '类名',
|
||||
setter: 'classNameSetter',
|
||||
group: 'style',
|
||||
},
|
||||
],
|
||||
},
|
||||
Columns: {
|
||||
name: 'Columns',
|
||||
type: 'container',
|
||||
icon: 'icon-column-4',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
childrenNames: ['Column'],
|
||||
},
|
||||
Column: {
|
||||
name: 'Column',
|
||||
type: 'container',
|
||||
icon: 'icon-juxing',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
siblingNames: ['Column'],
|
||||
},
|
||||
Text: {
|
||||
name: 'Text',
|
||||
type: 'element',
|
||||
icon: 'icon-wenzi',
|
||||
package: '@music163/antd',
|
||||
initChildren: '文本内容',
|
||||
},
|
||||
Placeholder: {
|
||||
name: 'Placeholder',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
},
|
||||
ButtonGroup: {
|
||||
name: 'ButtonGroup',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
childrenNames: ['Button'],
|
||||
},
|
||||
};
|
||||
|
||||
export default prototypes;
|
||||
@@ -0,0 +1,6 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
@import '~antd/dist/antd.less'; // 引入官方提供的 less 样式入口文件
|
||||
|
||||
@primary-color: #2f54eb;
|
||||
@border-radius-base: 2px;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'umi';
|
||||
import './index.less';
|
||||
|
||||
export default function Layout() {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const DocsPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>This is umi docs.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsPage;
|
||||
@@ -0,0 +1,219 @@
|
||||
import { Box } from 'coral-system';
|
||||
import { Button, Form, Input, Modal, Space } from 'antd';
|
||||
import {
|
||||
Designer,
|
||||
DesignerPanel,
|
||||
SettingPanel,
|
||||
Sidebar,
|
||||
Toolbar,
|
||||
WorkspacePanel,
|
||||
WorkspaceView,
|
||||
CodeEditor,
|
||||
Sandbox,
|
||||
DndQuery,
|
||||
themeLight,
|
||||
} from '@music163/tango-designer';
|
||||
import { createEngine, Workspace } from '@music163/tango-core';
|
||||
import prototypes from '../helpers/prototypes';
|
||||
import { Logo, ProjectDetail, bootHelperVariables, emptyPageCode, sampleFiles } from '../helpers';
|
||||
import {
|
||||
ApiOutlined,
|
||||
AppstoreAddOutlined,
|
||||
BuildOutlined,
|
||||
FunctionOutlined,
|
||||
PlusOutlined,
|
||||
createFromIconfontCN,
|
||||
} from '@ant-design/icons';
|
||||
import { Action, PackageOutlined } from '@music163/tango-ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
const menuData = {
|
||||
common: [
|
||||
{
|
||||
title: '常用',
|
||||
items: [
|
||||
'Button',
|
||||
'Section',
|
||||
'Columns',
|
||||
'Column',
|
||||
'Box',
|
||||
'Text',
|
||||
'Space',
|
||||
'Typography',
|
||||
'Title',
|
||||
'Paragraph',
|
||||
'Table',
|
||||
'Each',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '输入',
|
||||
items: ['Input', 'InputNumber', 'Select'],
|
||||
},
|
||||
{
|
||||
title: 'Formily表单',
|
||||
items: ['FormilyForm', 'FormilyFormItem', 'FormilySubmit', 'FormilyReset'],
|
||||
},
|
||||
{
|
||||
title: '数据展示',
|
||||
items: ['Comment'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 1. 实例化工作区
|
||||
const workspace = new Workspace({
|
||||
entry: '/src/index.js',
|
||||
files: sampleFiles,
|
||||
prototypes,
|
||||
});
|
||||
|
||||
// inject workspace to window for debug
|
||||
(window as any).__workspace__ = workspace;
|
||||
|
||||
// 2. 引擎初始化
|
||||
const engine = createEngine({
|
||||
workspace,
|
||||
menuData,
|
||||
defaultActiveView: 'design', // dual code design
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
window.__workspace__ = workspace;
|
||||
|
||||
// 3. 沙箱初始化
|
||||
const sandboxQuery = new DndQuery({
|
||||
context: 'iframe',
|
||||
});
|
||||
|
||||
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
/**
|
||||
* 5. 平台初始化,访问 https://local.netease.com:6006/
|
||||
*/
|
||||
export default function App() {
|
||||
const [showNewPageModal, setShowNewPageModal] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
return (
|
||||
<Designer
|
||||
theme={themeLight}
|
||||
engine={engine}
|
||||
config={{
|
||||
customActionVariables: bootHelperVariables,
|
||||
customExpressionVariables: bootHelperVariables,
|
||||
}}
|
||||
sandboxQuery={sandboxQuery}
|
||||
>
|
||||
<DesignerPanel
|
||||
logo={<Logo />}
|
||||
description={<ProjectDetail />}
|
||||
actions={
|
||||
<Box px="l">
|
||||
<Toolbar>
|
||||
<Toolbar.Item key="routeSwitch" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="addPage" placement="left" activeViews={['design']}>
|
||||
<Action
|
||||
tooltip="添加页面"
|
||||
shape="outline"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowNewPageModal(true)}
|
||||
/>
|
||||
</Toolbar.Item>
|
||||
<Toolbar.Item key="history" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="preview" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="togglePanel" placement="right" activeViews={['design']} />
|
||||
<Toolbar.Item key="modeSwitch" placement="right" />
|
||||
<Toolbar.Separator />
|
||||
<Toolbar.Item placement="right">
|
||||
<Space>
|
||||
<Button type="primary">发布</Button>
|
||||
</Space>
|
||||
</Toolbar.Item>
|
||||
</Toolbar>
|
||||
<Modal
|
||||
title="添加新页面"
|
||||
open={showNewPageModal}
|
||||
onCancel={() => setShowNewPageModal(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={(values) => {
|
||||
workspace.addViewFile(values.name, emptyPageCode);
|
||||
setShowNewPageModal(false);
|
||||
}}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item label="文件名" name="name" required rules={[{ required: true }]}>
|
||||
<Input placeholder="请输入文件名" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
提交
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Sidebar>
|
||||
<Sidebar.Item
|
||||
key="components"
|
||||
label="组件"
|
||||
icon={<AppstoreAddOutlined />}
|
||||
widgetProps={{
|
||||
menuData,
|
||||
}}
|
||||
/>
|
||||
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
|
||||
<Sidebar.Item
|
||||
key="variables"
|
||||
label="变量"
|
||||
icon={<FunctionOutlined />}
|
||||
isFloat
|
||||
width={800}
|
||||
/>
|
||||
<Sidebar.Item key="dataSource" label="接口" icon={<ApiOutlined />} isFloat width={800} />
|
||||
<Sidebar.Item
|
||||
key="dependency"
|
||||
label="依赖"
|
||||
icon={<PackageOutlined />}
|
||||
isFloat
|
||||
width={800}
|
||||
/>
|
||||
</Sidebar>
|
||||
<WorkspacePanel>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="design">
|
||||
<Sandbox
|
||||
onMessage={(e) => {
|
||||
if (e.type === 'done') {
|
||||
const sandboxWindow: any = sandboxQuery.window;
|
||||
// if (sandboxWindow.TangoAntd) {
|
||||
// if (sandboxWindow.TangoAntd.menuData) {
|
||||
// setMenuData(sandboxWindow.TangoAntd.menuData);
|
||||
// }
|
||||
// if (sandboxWindow.TangoAntd.prototypes) {
|
||||
// workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
|
||||
// }
|
||||
// }
|
||||
if (sandboxWindow.localTangoComponentPrototypes) {
|
||||
workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
|
||||
}
|
||||
}
|
||||
}}
|
||||
navigatorExtra={<Button size="small">hello world</Button>}
|
||||
/>
|
||||
</WorkspaceView>
|
||||
</WorkspacePanel>
|
||||
<SettingPanel />
|
||||
</DesignerPanel>
|
||||
</Designer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Button, Space } from 'antd';
|
||||
import {
|
||||
Designer,
|
||||
DesignerPanel,
|
||||
SettingPanel,
|
||||
Sidebar,
|
||||
Toolbar,
|
||||
WorkspacePanel,
|
||||
WorkspaceView,
|
||||
CodeEditor,
|
||||
Sandbox,
|
||||
DndQuery,
|
||||
themeLight,
|
||||
} from '@music163/tango-designer';
|
||||
import { createEngine, Workspace } from '@music163/tango-core';
|
||||
import { Logo, ProjectDetail, bootHelperVariables } from '@/helpers';
|
||||
import {
|
||||
AppstoreAddOutlined,
|
||||
BuildOutlined,
|
||||
ClusterOutlined,
|
||||
createFromIconfontCN,
|
||||
} from '@ant-design/icons';
|
||||
import { mailFiles } from '@/helpers/mail-files';
|
||||
|
||||
// 1. 实例化工作区
|
||||
const workspace = new Workspace({
|
||||
entry: '/src/index.js',
|
||||
files: mailFiles,
|
||||
});
|
||||
|
||||
// 2. 引擎初始化
|
||||
const engine = createEngine({
|
||||
workspace,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
window.__mailWorkspace__ = workspace;
|
||||
|
||||
// 3. 沙箱初始化
|
||||
const sandboxQuery = new DndQuery({
|
||||
context: 'iframe',
|
||||
});
|
||||
|
||||
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
/**
|
||||
* 5. 平台初始化,访问 https://local.netease.com:6006/
|
||||
*/
|
||||
export default function App() {
|
||||
const [menuLoading, setMenuLoading] = useState(true);
|
||||
const [menuData, setMenuData] = useState(false);
|
||||
return (
|
||||
<Designer
|
||||
theme={themeLight}
|
||||
engine={engine}
|
||||
config={{
|
||||
customActionVariables: bootHelperVariables,
|
||||
customExpressionVariables: bootHelperVariables,
|
||||
}}
|
||||
sandboxQuery={sandboxQuery}
|
||||
>
|
||||
<DesignerPanel
|
||||
logo={<Logo />}
|
||||
description={<ProjectDetail />}
|
||||
actions={
|
||||
<Box px="l">
|
||||
<Toolbar>
|
||||
<Toolbar.Item key="history" placement="left" />
|
||||
<Toolbar.Item key="preview" placement="left" />
|
||||
<Toolbar.Item key="modeSwitch" placement="right" />
|
||||
<Toolbar.Item key="togglePanel" placement="right" />
|
||||
<Toolbar.Separator />
|
||||
<Toolbar.Item placement="right">
|
||||
<Space>
|
||||
<Button type="primary">发布</Button>
|
||||
</Space>
|
||||
</Toolbar.Item>
|
||||
</Toolbar>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Sidebar>
|
||||
<Sidebar.Item
|
||||
key="components"
|
||||
label="组件"
|
||||
icon={<AppstoreAddOutlined />}
|
||||
widgetProps={{
|
||||
menuData: menuData as any,
|
||||
loading: menuLoading,
|
||||
}}
|
||||
/>
|
||||
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
|
||||
<Sidebar.Item
|
||||
key="dependency"
|
||||
label="依赖"
|
||||
icon={<ClusterOutlined />}
|
||||
isFloat
|
||||
width={800}
|
||||
/>
|
||||
</Sidebar>
|
||||
<WorkspacePanel>
|
||||
<WorkspaceView mode="design">
|
||||
<Sandbox
|
||||
onMessage={(e) => {
|
||||
if (e.type === 'done') {
|
||||
const sandboxWindow: any = sandboxQuery.window;
|
||||
if (sandboxWindow.TangoMail) {
|
||||
if (sandboxWindow.TangoMail.menuData) {
|
||||
setMenuData(sandboxWindow.TangoMail.menuData);
|
||||
engine.designer.setMenuData(sandboxWindow.TangoMail.menuData);
|
||||
}
|
||||
if (sandboxWindow.TangoMail.prototypes) {
|
||||
workspace.setComponentPrototypes(sandboxWindow.TangoMail.prototypes);
|
||||
}
|
||||
}
|
||||
setMenuLoading(false);
|
||||
}
|
||||
}}
|
||||
navigatorExtra={<Button size="small">hello world</Button>}
|
||||
/>
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
</WorkspacePanel>
|
||||
<SettingPanel />
|
||||
</DesignerPanel>
|
||||
</Designer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./src/.umi/tsconfig.json"
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
import 'umi/typings';
|
||||
@@ -0,0 +1,37 @@
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
|
||||
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
|
||||
|
||||
babel: async (config) => {
|
||||
config.plugins.push('babel-plugin-styled-components');
|
||||
return config;
|
||||
},
|
||||
|
||||
typescript: {
|
||||
reactDocgen: false,
|
||||
},
|
||||
|
||||
webpack: async (config) => {
|
||||
if (config.mode === 'production') {
|
||||
config.devtool = false;
|
||||
}
|
||||
|
||||
if (config.resolve.plugins === null) {
|
||||
config.resolve.plugins = [];
|
||||
}
|
||||
|
||||
config.resolve.plugins.push(new TsconfigPathsPlugin());
|
||||
|
||||
// @see https://github.com/graphql/graphql-js/issues/1272#issuecomment-393903706
|
||||
config.module.rules.push({
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: 'javascript/auto',
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<!--insert head here-->
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { SystemProvider } from 'coral-system';
|
||||
import 'antd/dist/antd.css';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on.*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const withSystemProvider = (Story, context) => {
|
||||
return (
|
||||
<SystemProvider prefix="--tango">
|
||||
<Story {...context} />
|
||||
</SystemProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const decorators = [withSystemProvider];
|
||||
@@ -0,0 +1,3 @@
|
||||
# `docs`
|
||||
|
||||
开发调试用的文档
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "storybook",
|
||||
"version": "0.0.0",
|
||||
"private": "true",
|
||||
"description": "> tango-apps docs",
|
||||
"license": "MIT",
|
||||
"author": "wwsun <sunweiwei01@corp.netease.com>",
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "echo \"skip\"",
|
||||
"build-storybook": "build-storybook",
|
||||
"storybook": "start-storybook -p 6008"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-setting-form": "*",
|
||||
"@music163/tango-ui": "*",
|
||||
"mobx": "6.13.2",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@storybook/addon-actions": "^6.5.14",
|
||||
"@storybook/addon-essentials": "^6.5.14",
|
||||
"@storybook/addon-links": "^6.5.14",
|
||||
"@storybook/react": "^6.5.14",
|
||||
"antd": "^4.24.2",
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { SingleMonacoEditor } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'Editor',
|
||||
};
|
||||
|
||||
const code = `
|
||||
import React from 'react';
|
||||
import { definePage } from '@music163/tango-boot';
|
||||
import { Layout, Page, Section, Button } from '@music163/antd';
|
||||
|
||||
function About(props) {
|
||||
const { stores } = props;
|
||||
|
||||
const increment = () => {
|
||||
stores.counter.increment();
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Page title="About Page" height="100vh">
|
||||
<Section>
|
||||
<h1>Counter: {stores.counter.num}</h1>
|
||||
<Button type="primary" onClick={increment}>
|
||||
+1
|
||||
</Button>
|
||||
<p>原生html元素不可拖拽</p>
|
||||
</Section>
|
||||
</Page>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default definePage(About);
|
||||
`;
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<Box border="solid" borderColor="line.normal" height="400px">
|
||||
<SingleMonacoEditor defaultValue={code} height="100%" onBlur={console.log} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { CodeSandbox } from '@music163/tango-sandbox';
|
||||
import { JsonView } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'CodeSandbox',
|
||||
};
|
||||
|
||||
const entryFilename = '/index.js';
|
||||
|
||||
const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
main: entryFilename,
|
||||
dependencies: {
|
||||
react: '^17.0.1',
|
||||
'react-dom': '^17.0.1',
|
||||
},
|
||||
};
|
||||
|
||||
const entryFile = `
|
||||
import React from 'react';
|
||||
import ReactDOM from "react-dom";
|
||||
import App from './src';
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
rootElement
|
||||
);
|
||||
`;
|
||||
|
||||
const srcAppFile = `
|
||||
import React from 'react';
|
||||
|
||||
export default function App() {
|
||||
return <div style={{ background: 'yellow', color: 'red' }}>hello world</div>;
|
||||
}
|
||||
`;
|
||||
|
||||
const files = {
|
||||
'/package.json': {
|
||||
code: JSON.stringify(packageJson),
|
||||
},
|
||||
[entryFilename]: {
|
||||
code: entryFile,
|
||||
},
|
||||
'/src/index.js': { code: srcAppFile },
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<Box display="flex" height="100vh">
|
||||
<CodeSandbox
|
||||
template="create-react-app"
|
||||
bundlerURL="https://codesandbox.fn.netease.com"
|
||||
files={files}
|
||||
entry={entryFilename}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>
|
||||
<Box width="30%">
|
||||
<JsonView src={files} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
import React from 'react';
|
||||
import { FormModel, SettingForm, register } from '@music163/tango-setting-form';
|
||||
import { IComponentPrototype } from '@music163/tango-helpers';
|
||||
import { BUILT_IN_SETTERS } from '@music163/tango-designer/src/setters';
|
||||
import { Box } from 'coral-system';
|
||||
import { JsonView } from '@music163/tango-ui';
|
||||
import { toJS } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Card } from 'antd';
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
const BLACK_LIST = ['codeSetter', 'eventSetter', 'modelSetter', 'routerSetter'];
|
||||
|
||||
BUILT_IN_SETTERS.filter((setter) => !BLACK_LIST.includes(setter.name)).forEach(register);
|
||||
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'SettingForm',
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单值预览
|
||||
*/
|
||||
const FormValuePreview = observer(({ model }: { model: FormModel }) => {
|
||||
const data = toJS(model.values);
|
||||
return <JsonView src={data} />;
|
||||
});
|
||||
|
||||
interface SettingFormDemoProps {
|
||||
initValues?: object;
|
||||
prototype?: IComponentPrototype;
|
||||
}
|
||||
|
||||
function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {
|
||||
const model = new FormModel(initValues, { onChange: console.log });
|
||||
return (
|
||||
<Box display="flex">
|
||||
<Box flex="0 0 320px" overflow="hidden">
|
||||
<SettingForm
|
||||
model={model}
|
||||
prototype={prototype}
|
||||
showIdentifier={{
|
||||
identifierKey: 'tid',
|
||||
getIdentifier: () => `time${Date.now()}`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box position="relative">
|
||||
<Card title="表单状态预览" style={{ position: 'sticky', top: 0 }}>
|
||||
<FormValuePreview model={model} />
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const prototypeHasBasicProps: IComponentPrototype = {
|
||||
name: 'Sample',
|
||||
title: '演示组件',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
|
||||
props: [
|
||||
{
|
||||
name: 'code',
|
||||
title: 'codeSetter',
|
||||
setter: 'codeSetter',
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
title: 'codeSetter(cssObject)',
|
||||
setter: 'codeSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'text2',
|
||||
title: 'textAreaSetter',
|
||||
setter: 'textAreaSetter',
|
||||
},
|
||||
{
|
||||
name: 'src',
|
||||
title: 'imageSetter',
|
||||
setter: 'imageSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
{
|
||||
name: 'number2',
|
||||
title: 'sliderSetter',
|
||||
setter: 'sliderSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool',
|
||||
title: 'boolSetter',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'enum',
|
||||
title: 'enumSetter',
|
||||
setter: 'enumSetter',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
bool: true,
|
||||
enum: {
|
||||
aaa: 'aaa',
|
||||
bbb: 'bbb',
|
||||
ccc: 'ccc',
|
||||
},
|
||||
list: [{ key: 1 }, { key: 2 }],
|
||||
}}
|
||||
prototype={prototypeHasBasicProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DeprecatedProp() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Deprecated',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
tip: '这是一个文本属性',
|
||||
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
|
||||
deprecated: '使用 text2 替代',
|
||||
},
|
||||
{
|
||||
name: 'number1',
|
||||
title: 'sliderSetter',
|
||||
setter: 'sliderSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Validate() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Validate',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
validate: (value) => {
|
||||
if (!value && value !== 0) {
|
||||
return '必填';
|
||||
}
|
||||
if (value < 0) {
|
||||
return '必须大于 0';
|
||||
}
|
||||
if (value > 10) {
|
||||
return '必须小于等于 10';
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ObjectSetter() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Object',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
title: 'objectSetter',
|
||||
setter: 'objectSetter',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function InitValues() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
bool: true,
|
||||
bool1: '{{true}}',
|
||||
style: {
|
||||
background: 'red',
|
||||
},
|
||||
object: {
|
||||
text: 'text',
|
||||
number: 10,
|
||||
},
|
||||
object1: {
|
||||
text: 'text',
|
||||
number: '{{tango.stores.user?.age}}',
|
||||
},
|
||||
// 只会有一种情况传下来的是字符串,就是用户代码里存在 rest operator,这时候不需要额外处理,提示用户就使用代码模式
|
||||
object2: '{{{ text: "text22", number: 22, ...{ extra: "some" } }}}',
|
||||
list: [{ key: 'aaa' }, { key: 'bbb' }], // list object
|
||||
list1: "{{[{ key: 'aaa' }, { key: 'bbb' }]}}", // raw code
|
||||
}}
|
||||
prototype={{
|
||||
name: 'InitValues',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'bool',
|
||||
title: 'value初始化',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool1',
|
||||
title: 'value初始化',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool2',
|
||||
title: '无初值',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
title: 'codeSetter',
|
||||
setter: 'codeSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'object1',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'object2',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
{
|
||||
name: 'list1',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Lite() {
|
||||
return (
|
||||
<Box width={320} border="solid" borderColor="line2">
|
||||
<SettingForm
|
||||
showSearch={false}
|
||||
showGroups={false}
|
||||
showItemSubtitle={false}
|
||||
prototype={prototypeHasBasicProps}
|
||||
disableSwitchExpressionSetter
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function HideToggleCode() {
|
||||
const model = new FormModel({});
|
||||
return (
|
||||
<Box width={320} border="solid" borderColor="line2">
|
||||
<SettingForm model={model} prototype={prototypeHasBasicProps} disableSwitchExpressionSetter />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const prototypeHasExtraProps: IComponentPrototype = {
|
||||
name: 'ExtraProps',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
props: [
|
||||
{
|
||||
name: 'choice',
|
||||
title: 'choiceSetter',
|
||||
setter: 'choiceSetter',
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
{ label: '选项3', value: '3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'picker',
|
||||
title: 'pickerSetter',
|
||||
setter: 'pickerSetter',
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
{ label: '选项3', value: '3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'actionList',
|
||||
title: 'actionListSetter',
|
||||
setter: 'actionListSetter',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
title: 'optionSetter',
|
||||
setter: 'optionSetter',
|
||||
},
|
||||
{
|
||||
name: 'columns',
|
||||
title: 'tableColumnsSetter',
|
||||
setter: 'tableColumnsSetter',
|
||||
},
|
||||
{
|
||||
name: 'css',
|
||||
title: 'cssSetter',
|
||||
setter: 'cssSetter',
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
title: 'dateSetter',
|
||||
setter: 'dateSetter',
|
||||
},
|
||||
{
|
||||
name: 'dateRange',
|
||||
title: 'dateRangeSetter',
|
||||
setter: 'dateRangeSetter',
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
title: 'timeSetter',
|
||||
setter: 'timeSetter',
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
title: 'timeRangeSetter',
|
||||
setter: 'timeRangeSetter',
|
||||
},
|
||||
{
|
||||
name: 'enum',
|
||||
title: 'enumSetter',
|
||||
setter: 'enumSetter',
|
||||
},
|
||||
{
|
||||
name: 'event',
|
||||
title: 'eventSetter',
|
||||
setter: 'eventSetter',
|
||||
},
|
||||
{
|
||||
name: 'json',
|
||||
title: 'jsonSetter',
|
||||
setter: 'jsonSetter',
|
||||
},
|
||||
{
|
||||
name: 'jsx',
|
||||
title: 'jsxSetter',
|
||||
setter: 'jsxSetter',
|
||||
},
|
||||
{
|
||||
name: 'render',
|
||||
title: 'renderPropsSetter',
|
||||
setter: 'renderPropsSetter',
|
||||
},
|
||||
{
|
||||
name: 'cell',
|
||||
title: 'tableCellSetter',
|
||||
setter: 'tableCellSetter',
|
||||
},
|
||||
{
|
||||
name: 'expandable',
|
||||
title: 'tableExpandableSetter',
|
||||
setter: 'tableExpandableSetter',
|
||||
},
|
||||
{
|
||||
name: 'router',
|
||||
title: 'routerSetter',
|
||||
setter: 'routerSetter',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function ExtraSetters() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
router: 'www.163.com',
|
||||
expression: `{ foo: 'foo' }`,
|
||||
object: {
|
||||
name: 'Alice',
|
||||
},
|
||||
image:
|
||||
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
|
||||
}}
|
||||
prototype={prototypeHasExtraProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function StyleProps() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Box',
|
||||
title: 'Box',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
props: [
|
||||
{
|
||||
name: 'style',
|
||||
title: 'styleSetter',
|
||||
setter: 'styleSetter',
|
||||
},
|
||||
{
|
||||
name: 'display',
|
||||
title: 'displaySetter',
|
||||
setter: 'displaySetter',
|
||||
},
|
||||
{
|
||||
name: 'flexDirection',
|
||||
title: 'flexDirectionSetter',
|
||||
setter: 'flexDirectionSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexGap',
|
||||
title: 'flexGapSetter',
|
||||
setter: 'flexGapSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexJustifyContent',
|
||||
title: 'flexJustifyContentSetter',
|
||||
setter: 'flexJustifyContentSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexAlignItems',
|
||||
title: 'flexAlignItemsSetter',
|
||||
setter: 'flexAlignItemsSetter',
|
||||
},
|
||||
{
|
||||
name: 'spacing',
|
||||
title: 'spacingSetter',
|
||||
setter: 'spacingSetter',
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
title: 'colorSetter',
|
||||
setter: 'colorSetter',
|
||||
},
|
||||
{
|
||||
name: 'bg',
|
||||
title: 'bgSetter',
|
||||
setter: 'bgSetter',
|
||||
},
|
||||
{
|
||||
name: 'border',
|
||||
title: 'borderSetter',
|
||||
setter: 'borderSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/ActionSelect',
|
||||
component: ActionSelect,
|
||||
};
|
||||
|
||||
const options = [
|
||||
{ label: 'action1', value: 'action1' },
|
||||
{ label: 'action2', value: 'action2' },
|
||||
{ label: 'action3', value: 'action3' },
|
||||
];
|
||||
|
||||
export const Basic = {
|
||||
args: {
|
||||
defaultText: '选择动作',
|
||||
options,
|
||||
onSelect: console.log,
|
||||
},
|
||||
};
|
||||
|
||||
export const showInput = {
|
||||
args: {
|
||||
text: '选择动作',
|
||||
options,
|
||||
showInput: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { Action } from '@music163/tango-ui';
|
||||
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { Space } from 'antd';
|
||||
|
||||
export default {
|
||||
title: 'UI/Action',
|
||||
component: Action,
|
||||
};
|
||||
|
||||
export const Basic = {
|
||||
args: {
|
||||
icon: <PlusOutlined />,
|
||||
tooltip: '添加服务函数',
|
||||
},
|
||||
};
|
||||
|
||||
export const Link = {
|
||||
args: {
|
||||
icon: <QuestionCircleOutlined />,
|
||||
tooltip: '查看帮助文档',
|
||||
href: 'https://music.163.com',
|
||||
},
|
||||
};
|
||||
|
||||
export const Outline = {
|
||||
args: {
|
||||
icon: <PlusOutlined />,
|
||||
tooltip: '添加服务函数',
|
||||
shape: 'outline',
|
||||
},
|
||||
};
|
||||
|
||||
export const Small = {
|
||||
args: {
|
||||
size: 'small',
|
||||
icon: <PlusOutlined />,
|
||||
tooltip: '添加服务函数',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = () => {
|
||||
return (
|
||||
<Space>
|
||||
<Action icon={<PlusOutlined />} tooltip="添加服务函数" disabled />
|
||||
<Action icon={<PlusOutlined />} tooltip="添加服务函数" disabled shape="outline" />
|
||||
<Action
|
||||
icon={<QuestionCircleOutlined />}
|
||||
tooltip="查看帮助文档"
|
||||
href="https://music.163.com"
|
||||
disabled
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = () => {
|
||||
return (
|
||||
<Space>
|
||||
<Action icon={<PlusOutlined />} tooltip="添加服务函数" />
|
||||
<Action
|
||||
icon={<QuestionCircleOutlined />}
|
||||
tooltip="查看帮助文档"
|
||||
href="https://music.163.com"
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { ChatInput } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/ChatGPT',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return <ChatInput />;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ClassNameInput } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/ClassNameInput',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return <ClassNameInput defaultValue="hello world" onChange={console.log} />;
|
||||
}
|
||||
|
||||
export function Controlled() {
|
||||
const [value, setValue] = useState('');
|
||||
return <ClassNameInput value={value} onChange={setValue} />;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { CopyClipboard } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/Copy',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<CopyClipboard text="hello">
|
||||
{(copied) => <button>{copied ? 'copied' : 'copy'}</button>}
|
||||
</CopyClipboard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { DragPanel } from '@music163/tango-ui';
|
||||
import { Box, Text } from 'coral-system';
|
||||
import { Button } from 'antd';
|
||||
|
||||
export default {
|
||||
title: 'UI/DragPanel',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<>
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
}}
|
||||
>
|
||||
底部浮层自动修正弹出区域
|
||||
</Button>
|
||||
</DragPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FooterCustom() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={(close) => (
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>底部信息</Text>
|
||||
<Button size="small" onClick={() => close()}>
|
||||
点此关闭
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResizeablePanel() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
resizeable
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
height={400}
|
||||
body={
|
||||
<Box p="m" textAlign="center" background="#fa8484">
|
||||
内容
|
||||
</Box>
|
||||
}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { Action, InputCode, InputStyleCode } from '@music163/tango-ui';
|
||||
import { BlockOutlined } from '@ant-design/icons';
|
||||
|
||||
export default {
|
||||
title: 'UI/InputCode',
|
||||
};
|
||||
|
||||
const context = {
|
||||
stores: {
|
||||
foo: {
|
||||
loading: false,
|
||||
action: () => {},
|
||||
},
|
||||
},
|
||||
services: {
|
||||
list: () => {},
|
||||
get: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<InputCode
|
||||
suffix={<Action icon={<BlockOutlined />} size="small" />}
|
||||
autoCompleteContext={{ tango: context }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CSS() {
|
||||
return <InputStyleCode enableESLint suffix={<Action icon={<BlockOutlined />} size="small" />} />;
|
||||
}
|
||||
|
||||
const code = `
|
||||
function foo() {
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
}
|
||||
`;
|
||||
|
||||
export function Readonly() {
|
||||
return <InputCode shape="inset" readOnly value={code} />;
|
||||
}
|
||||
|
||||
export function Inset() {
|
||||
return <InputCode shape="inset" />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { InputList } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/InputList',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
const [value, setValue] = useState([]);
|
||||
return <InputList value={value} onChange={setValue} />;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Menu } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/Menu',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<Menu
|
||||
activeKey="2"
|
||||
items={[
|
||||
{ key: '1', label: 'bob', note: 'male', deletable: true },
|
||||
{ key: '2', label: 'alice', note: 'female', deletable: true },
|
||||
{ key: '3', label: 'tom', deletable: true },
|
||||
]}
|
||||
></Menu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useState } from 'react';
|
||||
import { SelectList } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/SelectList',
|
||||
};
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: 'alice',
|
||||
label: 'Alice',
|
||||
},
|
||||
{
|
||||
value: 'jack',
|
||||
label: 'Jack',
|
||||
},
|
||||
{
|
||||
value: 'lucy',
|
||||
label: 'Lucy',
|
||||
},
|
||||
{
|
||||
value: 'bob',
|
||||
label: 'Bob',
|
||||
},
|
||||
];
|
||||
|
||||
export function Basic() {
|
||||
const [value, setValue] = useState([]);
|
||||
return <SelectList value={value} onChange={setValue} options={options} />;
|
||||
}
|
||||
|
||||
export function UniqueValue() {
|
||||
const [value, setValue] = useState([]);
|
||||
return <SelectList isUniqueValue value={value} onChange={setValue} options={options} />;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { TagSelect } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/TagSelect',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<TagSelect
|
||||
options={['Movies', 'Books', 'Music', 'Sports'].map((item) => ({ label: item, value: item }))}
|
||||
onChange={console.log}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SingleMode() {
|
||||
return (
|
||||
<TagSelect
|
||||
options={['Movies', 'Books', 'Music', 'Sports'].map((item) => ({ label: item, value: item }))}
|
||||
mode="single"
|
||||
onChange={console.log}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import { ToggleButton, UndoOutlined, RedoOutlined } from '@music163/tango-ui';
|
||||
import { Box, Group, Flex } from 'coral-system';
|
||||
|
||||
export default {
|
||||
title: 'UI/ToggleButton',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex" columnGap="l" p="l">
|
||||
<ToggleButton>
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton disabled>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
<Box display="flex" columnGap="l" p="l">
|
||||
<ToggleButton type="primary">
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton type="primary" selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton type="primary" disabled>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function DarkMode() {
|
||||
return (
|
||||
<Box>
|
||||
<Flex gap="l" bg="#222" p="l">
|
||||
<ToggleButton shape="ghost">
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="ghost" selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="ghost" disabled>
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
</Flex>
|
||||
<Flex gap="l" bg="#222" p="l">
|
||||
<ToggleButton shape="text">
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="text" selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="text" disabled>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function ButtonGroup() {
|
||||
return (
|
||||
<Group attached>
|
||||
<ToggleButton>
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton disabled>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { VariableTree } from '@music163/tango-designer/src/components';
|
||||
import { Box } from 'coral-system';
|
||||
|
||||
export default {
|
||||
title: 'Designer/VariableTree',
|
||||
};
|
||||
|
||||
const data = [
|
||||
{
|
||||
title: 'Stores',
|
||||
key: 'stores',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
title: 'user',
|
||||
key: 'stores.user',
|
||||
children: [
|
||||
{
|
||||
title: 'name',
|
||||
key: 'stores.user.name',
|
||||
raw: '"Tom"',
|
||||
},
|
||||
{
|
||||
title: 'age',
|
||||
key: 'stores.user.age',
|
||||
raw: '18',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'book',
|
||||
key: 'stores.book',
|
||||
children: [
|
||||
{
|
||||
title: 'name',
|
||||
key: 'stores.book.name',
|
||||
raw: '"JavaScript 高级程序设计"',
|
||||
},
|
||||
{
|
||||
title: 'price',
|
||||
key: 'stores.book.price',
|
||||
raw: '99.99',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Services',
|
||||
key: 'services',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
title: 'add',
|
||||
key: 'services.add',
|
||||
},
|
||||
{
|
||||
title: 'multiply',
|
||||
key: 'services.multiply',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<Box width={600} border="solid">
|
||||
<VariableTree dataSource={data} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "esnext"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* BabelConfig for Jest
|
||||
*/
|
||||
module.exports = {
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-react', '@babel/preset-typescript'],
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'scope-case': [0],
|
||||
'subject-case': [0],
|
||||
},
|
||||
};
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* For a detailed explanation regarding each configuration property, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
const config = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/q_/7k4h88k50pn0p6423z7smp9r0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls, instances, contexts and results before every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
collectCoverage: true,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
// coverageProvider: "babel",
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// The default configuration for fake timers
|
||||
// fakeTimers: {
|
||||
// "enableGlobally": false
|
||||
// },
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "mjs",
|
||||
// "cjs",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "json",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state before every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state and implementation before every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: 'jsdom',
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: ['**/tests/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jest-circus/runner",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/",
|
||||
// "\\.pnp\\.[^\\/]+$"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "independent",
|
||||
"packages": ["packages/*"],
|
||||
"npmClient": "yarn",
|
||||
"command": {
|
||||
"version": {
|
||||
"message": "chore(release): publish"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "tango-community",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"apps/*"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "yarn workspace playground dev",
|
||||
"start:docs": "yarn workspace storybook storybook",
|
||||
"build": "lerna run build",
|
||||
"typedoc": "typedoc",
|
||||
"docs": "yarn typedoc && open docs/index.html",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
|
||||
"publish": "lerna publish",
|
||||
"ver": "lerna version --no-private --conventional-commits",
|
||||
"release": "yarn eslint && yarn build && yarn ver && lerna publish from-git",
|
||||
"release:npm": "yarn eslint && yarn build && yarn ver && lerna publish from-package",
|
||||
"release:beta": "yarn eslint && yarn build && yarn ver && lerna publish from-package --dist-tag beta",
|
||||
"up": "yarn upgrade-interactive --latest",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@commitlint/cli": "^18.4.3",
|
||||
"@commitlint/config-conventional": "^18.4.3",
|
||||
"@types/jest": "^29.5.7",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/lodash.get": "^4.4.8",
|
||||
"@types/lodash.isequal": "^4.5.7",
|
||||
"@types/lodash.set": "^4.3.8",
|
||||
"@types/react": "^18.2.34",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"@types/styled-components": "^5.1.29",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"conventional-changelog-cli": "^4.1.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-ali": "^14.0.2",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"http-server": "^14.1.0",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"lerna": "^8.1.2",
|
||||
"lint-staged": "^15.2.2",
|
||||
"prettier": "^3.2.5",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"styled-components": "^5.3.6",
|
||||
"typedoc": "^0.26.7",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.10](https://github.com/netease/tango/compare/@music163/tango-context@1.1.9...@music163/tango-context@1.1.10) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- service support set description field ([#206](https://github.com/netease/tango/issues/206)) ([79f9a8c](https://github.com/netease/tango/commit/79f9a8c1791424f35f194ab2a9aa7c263ef39f40))
|
||||
|
||||
## [1.1.9](https://github.com/netease/tango/compare/@music163/tango-context@1.1.8...@music163/tango-context@1.1.9) (2024-09-02)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.8](https://github.com/netease/tango/compare/@music163/tango-context@1.1.7...@music163/tango-context@1.1.8) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.7](https://github.com/netease/tango/compare/@music163/tango-context@1.1.6...@music163/tango-context@1.1.7) (2024-08-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.6](https://github.com/netease/tango/compare/@music163/tango-context@1.1.5...@music163/tango-context@1.1.6) (2024-08-01)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.5](https://github.com/netease/tango/compare/@music163/tango-context@1.1.4...@music163/tango-context@1.1.5) (2024-07-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.4](https://github.com/netease/tango/compare/@music163/tango-context@1.1.3...@music163/tango-context@1.1.4) (2024-06-20)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.3](https://github.com/netease/tango/compare/@music163/tango-context@1.1.2...@music163/tango-context@1.1.3) (2024-06-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.2](https://github.com/netease/tango/compare/@music163/tango-context@1.1.1...@music163/tango-context@1.1.2) (2024-06-03)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-context@1.1.0...@music163/tango-context@1.1.1) (2024-05-21)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-context@1.0.2...@music163/tango-context@1.1.0) (2024-05-17)
|
||||
|
||||
### Features
|
||||
|
||||
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||
|
||||
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0...@music163/tango-context@1.0.1) (2024-04-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
|
||||
|
||||
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.9...@music163/tango-context@1.0.0-alpha.10) (2024-04-09)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.9](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.8...@music163/tango-context@1.0.0-alpha.9) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.8](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.7...@music163/tango-context@1.0.0-alpha.8) (2024-03-18)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.6...@music163/tango-context@1.0.0-alpha.7) (2024-03-18)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.5...@music163/tango-context@1.0.0-alpha.6) (2024-01-16)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.4...@music163/tango-context@1.0.0-alpha.5) (2023-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
|
||||
|
||||
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.3...@music163/tango-context@1.0.0-alpha.4) (2023-12-26)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.2...@music163/tango-context@1.0.0-alpha.3) (2023-12-25)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-context@0.2.12...@music163/tango-context@1.0.0-alpha.1) (2023-12-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.12](https://github.com/netease/tango/compare/@music163/tango-context@0.2.11...@music163/tango-context@0.2.12) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-context@0.2.10...@music163/tango-context@0.2.11) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-context@0.2.9...@music163/tango-context@0.2.10) (2023-11-30)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-context@0.2.8...@music163/tango-context@0.2.9) (2023-11-29)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.8](https://github.com/netease/tango/compare/@music163/tango-context@0.2.7...@music163/tango-context@0.2.8) (2023-11-27)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-context@0.2.6...@music163/tango-context@0.2.7) (2023-11-23)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-context@0.2.5...@music163/tango-context@0.2.6) (2023-11-20)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-context@0.2.4...@music163/tango-context@0.2.5) (2023-11-16)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-context@0.2.3...@music163/tango-context@0.2.4) (2023-11-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- optional context config ([70fc6f8](https://github.com/netease/tango/commit/70fc6f8030202f5d340281c0424752044ed0fb83))
|
||||
|
||||
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-context@0.2.2...@music163/tango-context@0.2.3) (2023-10-23)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-context@0.2.1...@music163/tango-context@0.2.2) (2023-10-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- refactor expSetter ([#41](https://github.com/netease/tango/issues/41)) ([d63517e](https://github.com/netease/tango/commit/d63517ecb936e4227e70c33e610664316625f4f4))
|
||||
|
||||
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-context@0.2.0...@music163/tango-context@0.2.1) (2023-09-22)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-context@0.1.4...@music163/tango-context@0.2.0) (2023-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
|
||||
|
||||
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-context@0.1.3...@music163/tango-context@0.1.4) (2023-09-18)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-context@0.1.2...@music163/tango-context@0.1.3) (2023-09-13)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## 0.1.2 (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
@@ -0,0 +1,22 @@
|
||||
# tango-apps-context
|
||||
|
||||
> React context of tango-apps core
|
||||
|
||||
## Usage
|
||||
|
||||
install
|
||||
|
||||
```bash
|
||||
yarn add @music163/tango-context
|
||||
```
|
||||
|
||||
usage
|
||||
|
||||
```jsx
|
||||
import { observer, useWorkspace } from '@music/tango-apps-context';
|
||||
|
||||
export const SampleWidget = observer(() => {
|
||||
const ws = useWorkspace();
|
||||
return <div></div>;
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "1.1.10",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
"hooks"
|
||||
],
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
"license": "MIT",
|
||||
"main": "lib/cjs/index.js",
|
||||
"module": "lib/esm/index.js",
|
||||
"types": "lib/esm/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/netease/tango.git"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf lib/",
|
||||
"build": "yarn clean && yarn build:esm && yarn build:cjs",
|
||||
"build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020",
|
||||
"build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS",
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^1.4.4",
|
||||
"@music163/tango-helpers": "^1.2.4",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import type { AbstractCodeWorkspace, Engine } from '@music163/tango-core';
|
||||
import { IVariableTreeNode, createContext } from '@music163/tango-helpers';
|
||||
|
||||
export interface ITangoEngineContext {
|
||||
/**
|
||||
* 低代码引擎
|
||||
*/
|
||||
engine: Engine;
|
||||
/**
|
||||
* 自定义配置数据
|
||||
*/
|
||||
config?: {
|
||||
customActionVariables?: IVariableTreeNode[];
|
||||
customExpressionVariables?: IVariableTreeNode[];
|
||||
};
|
||||
}
|
||||
|
||||
const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>({
|
||||
name: 'TangoEngineContext',
|
||||
});
|
||||
|
||||
export { TangoEngineProvider };
|
||||
|
||||
/**
|
||||
* 获取 CodeWorkspace 实例
|
||||
* @returns
|
||||
*/
|
||||
export const useWorkspace = () => {
|
||||
return useTangoEngine()?.engine.workspace as AbstractCodeWorkspace;
|
||||
};
|
||||
|
||||
export const useDesigner = () => {
|
||||
return useTangoEngine()?.engine.designer;
|
||||
};
|
||||
|
||||
export const useWorkspaceData = () => {
|
||||
const ctx = useTangoEngine();
|
||||
const workspace = useWorkspace();
|
||||
const modelVariables: IVariableTreeNode[] = []; // 绑定变量列表
|
||||
const storeActionVariables: IVariableTreeNode[] = []; // 模型中的所有 actions
|
||||
const storeVariables: IVariableTreeNode[] = []; // 模型中的所有变量
|
||||
const pageStoreVariables: IVariableTreeNode[] = []; // 页面中的组件实例变量
|
||||
const serviceVariables: IVariableTreeNode[] = []; // 服务中的所有变量
|
||||
|
||||
pageStoreVariables.push({
|
||||
title: 'pageStore',
|
||||
key: 'pageStore',
|
||||
});
|
||||
|
||||
Object.values(workspace.storeModules).forEach((file) => {
|
||||
const prefix = `stores.${file.name}`;
|
||||
const states: IVariableTreeNode[] = file.states.map((item) => ({
|
||||
title: item.name,
|
||||
key: `${prefix}.${item.name}`,
|
||||
raw: item.code,
|
||||
showRemoveButton: true,
|
||||
}));
|
||||
const actions: IVariableTreeNode[] = file.actions.map((item) => ({
|
||||
title: item.name,
|
||||
key: `${prefix}.${item.name}`,
|
||||
type: 'function',
|
||||
raw: item.code,
|
||||
showRemoveButton: true,
|
||||
}));
|
||||
|
||||
modelVariables.push({
|
||||
title: file.name,
|
||||
key: prefix,
|
||||
selectable: false,
|
||||
children: states,
|
||||
showAddButton: true,
|
||||
});
|
||||
|
||||
storeActionVariables.push({
|
||||
title: file.name,
|
||||
key: prefix,
|
||||
selectable: false,
|
||||
children: actions,
|
||||
});
|
||||
|
||||
storeVariables.push({
|
||||
title: file.name,
|
||||
key: prefix,
|
||||
selectable: false,
|
||||
children: [...states, ...actions],
|
||||
showAddButton: true,
|
||||
});
|
||||
});
|
||||
|
||||
Object.values(workspace.serviceModules).forEach((file) => {
|
||||
const prefix = file.name !== 'index' ? `services.${file.name}` : 'services';
|
||||
serviceVariables.push({
|
||||
title: file.name,
|
||||
key: prefix,
|
||||
selectable: false,
|
||||
showAddButton: true,
|
||||
children: Object.entries(file.serviceFunctions || {}).map(([key, value]) => {
|
||||
const title = value.description ? `${key}(${value.description})` : key;
|
||||
return {
|
||||
title,
|
||||
key: [prefix, key].join('.'),
|
||||
type: 'function',
|
||||
showRemoveButton: true,
|
||||
};
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// 路由选项列表
|
||||
const routeOptions = workspace.pages?.map((item) => ({
|
||||
label: `${item.name} (${item.path})`,
|
||||
value: item.path,
|
||||
}));
|
||||
|
||||
let actionVariables: IVariableTreeNode[] = [
|
||||
buildVariableOptions('数据模型', '$stores', storeActionVariables),
|
||||
buildVariableOptions('服务函数', '$services', serviceVariables),
|
||||
];
|
||||
|
||||
if (ctx.config?.customActionVariables) {
|
||||
actionVariables = actionVariables.concat(ctx.config?.customActionVariables);
|
||||
}
|
||||
|
||||
let expressionVariables: IVariableTreeNode[] = [
|
||||
buildVariableOptions('当前页面组件实例', '$pageStore', pageStoreVariables),
|
||||
buildVariableOptions('数据模型', '$stores', storeVariables),
|
||||
buildVariableOptions('服务函数', '$services', serviceVariables),
|
||||
];
|
||||
if (ctx.config?.customExpressionVariables) {
|
||||
expressionVariables = expressionVariables.concat(ctx.config?.customExpressionVariables);
|
||||
}
|
||||
|
||||
return {
|
||||
modelVariables: [buildVariableOptions('数据模型', 'stores', modelVariables)],
|
||||
actionVariables,
|
||||
storeVariables,
|
||||
serviceVariables,
|
||||
expressionVariables,
|
||||
routeOptions,
|
||||
};
|
||||
};
|
||||
|
||||
function buildVariableOptions(title: string, key: string, children: IVariableTreeNode[]) {
|
||||
return {
|
||||
key,
|
||||
title,
|
||||
children,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { observer } from 'mobx-react-lite';
|
||||
export * from './context';
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.prod.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-core@1.4.3...@music163/tango-core@1.4.4) (2024-09-09)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
## [1.4.3](https://github.com/netease/tango/compare/@music163/tango-core@1.4.2...@music163/tango-core@1.4.3) (2024-09-02)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-core@1.4.1...@music163/tango-core@1.4.2) (2024-08-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- view module \_nodesTree set init value ([#194](https://github.com/netease/tango/issues/194)) ([269e304](https://github.com/netease/tango/commit/269e304d685c4fcc80635b9d04220cda333b80c4))
|
||||
|
||||
## [1.4.1](https://github.com/netease/tango/compare/@music163/tango-core@1.4.0...@music163/tango-core@1.4.1) (2024-08-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-core@1.3.3...@music163/tango-core@1.4.0) (2024-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
|
||||
|
||||
### Features
|
||||
|
||||
- add isError and errorMessage to File & add isAstSynced to Module ([#190](https://github.com/netease/tango/issues/190)) ([8dd289c](https://github.com/netease/tango/commit/8dd289cd7daba628e54dc6b8929f22a1e2245160))
|
||||
|
||||
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-core@1.3.2...@music163/tango-core@1.3.3) (2024-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
|
||||
|
||||
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-core@1.3.1...@music163/tango-core@1.3.2) (2024-06-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
|
||||
|
||||
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-core@1.3.0...@music163/tango-core@1.3.1) (2024-06-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- set value as jsxElement children ([#173](https://github.com/netease/tango/issues/173)) ([ae04850](https://github.com/netease/tango/commit/ae04850bb251f392575a72ca5fd960249ea9d06b))
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-core@1.2.0...@music163/tango-core@1.3.0) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-core@1.1.0...@music163/tango-core@1.2.0) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
|
||||
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
|
||||
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
|
||||
|
||||
### Features
|
||||
|
||||
- add select parent node of selected node ([#158](https://github.com/netease/tango/issues/158)) ([fe31246](https://github.com/netease/tango/commit/fe3124648325e72abfc58da8b2f8ff83301d40b8))
|
||||
- supoort set default active view ([#157](https://github.com/netease/tango/issues/157)) ([f854f75](https://github.com/netease/tango/commit/f854f75b8a8c25384d290bd76d6b8bb6fb69f22d))
|
||||
|
||||
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-core@1.0.2...@music163/tango-core@1.1.0) (2024-05-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse module with module alias ([#147](https://github.com/netease/tango/issues/147)) ([56d0877](https://github.com/netease/tango/commit/56d0877507b0138877eac6f36db288147b43c7d9))
|
||||
|
||||
### Features
|
||||
|
||||
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||
|
||||
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0...@music163/tango-core@1.0.1) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.9...@music163/tango-core@1.0.0-alpha.10) (2024-04-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update setters and use tabOptions to filter props ([#129](https://github.com/netease/tango/issues/129)) ([93608d1](https://github.com/netease/tango/commit/93608d1037327afa4f755976b86427b6128ae3d0))
|
||||
|
||||
# [1.0.0-alpha.9](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.8...@music163/tango-core@1.0.0-alpha.9) (2024-03-26)
|
||||
|
||||
### Features
|
||||
|
||||
- quick add sibling nodes ([#127](https://github.com/netease/tango/issues/127)) ([9ed6a7d](https://github.com/netease/tango/commit/9ed6a7d1a4944d69d96e034f243b61531862e317))
|
||||
|
||||
# [1.0.0-alpha.8](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.7...@music163/tango-core@1.0.0-alpha.8) (2024-03-18)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.6...@music163/tango-core@1.0.0-alpha.7) (2024-03-18)
|
||||
|
||||
### Features
|
||||
|
||||
- support code id ([#111](https://github.com/netease/tango/issues/111)) ([6c65362](https://github.com/netease/tango/commit/6c65362a5d5b2297b22f30c093c7d21a979630a1))
|
||||
|
||||
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.5...@music163/tango-core@1.0.0-alpha.6) (2024-01-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 优化变量树实现 ([#90](https://github.com/netease/tango/issues/90)) ([62d403f](https://github.com/netease/tango/commit/62d403f80a5ad08c216bc0c035ffc12c2cf329d2))
|
||||
|
||||
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.4...@music163/tango-core@1.0.0-alpha.5) (2023-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
|
||||
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
|
||||
|
||||
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.3...@music163/tango-core@1.0.0-alpha.4) (2023-12-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
|
||||
|
||||
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.2...@music163/tango-core@1.0.0-alpha.3) (2023-12-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export componentEntry module ([149002d](https://github.com/netease/tango/commit/149002ddddaee859333469f65c054d811bde2f7f))
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-core@0.2.11...@music163/tango-core@1.0.0-alpha.1) (2023-12-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-core@0.2.10...@music163/tango-core@0.2.11) (2023-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update FileType ([c884712](https://github.com/netease/tango/commit/c8847123312bc47ea06575f4e714dfd596893e39))
|
||||
|
||||
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-core@0.2.9...@music163/tango-core@0.2.10) (2023-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add filesFormatter to sandbox & fix update props with imports ([#71](https://github.com/netease/tango/issues/71)) ([138cbe9](https://github.com/netease/tango/commit/138cbe9b203b370aff42c1ae8086d69edacf35e9))
|
||||
|
||||
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-core@0.2.8...@music163/tango-core@0.2.9) (2023-11-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix render components tree ([76a5a2c](https://github.com/netease/tango/commit/76a5a2c65920bc42b019cd1f32a3cacd0d888638))
|
||||
|
||||
## [0.2.8](https://github.com/netease/tango/compare/@music163/tango-core@0.2.7...@music163/tango-core@0.2.8) (2023-11-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- workspace onFilesChange ([#70](https://github.com/netease/tango/issues/70)) ([accd826](https://github.com/netease/tango/commit/accd8263764c811ea8175a9cd341fc6fa6c75967))
|
||||
|
||||
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-core@0.2.6...@music163/tango-core@0.2.7) (2023-11-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- list service functions ([91b53da](https://github.com/netease/tango/commit/91b53da38e362ae320457429037dc347adf90bd3))
|
||||
- parse tango variables in view file ([d917633](https://github.com/netease/tango/commit/d91763379c4ccf9d826b717aa80a07abdaf2e3a2))
|
||||
|
||||
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-core@0.2.5...@music163/tango-core@0.2.6) (2023-11-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- prototype to import declaration ([#67](https://github.com/netease/tango/issues/67)) ([8a1234e](https://github.com/netease/tango/commit/8a1234e565489c92f041356b7a06339eaeee48df))
|
||||
- refactor update import specifiers ([#68](https://github.com/netease/tango/issues/68)) ([558f2cd](https://github.com/netease/tango/commit/558f2cd0a692c6bbc866d08250d25e2619af183f))
|
||||
|
||||
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-core@0.2.4...@music163/tango-core@0.2.5) (2023-11-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- refactor parse expression ([#61](https://github.com/netease/tango/issues/61)) ([dbbd1dd](https://github.com/netease/tango/commit/dbbd1dddc75c532b7c9710ab0941c8680100f093))
|
||||
|
||||
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-core@0.2.3...@music163/tango-core@0.2.4) (2023-11-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
|
||||
- export isJSXElementById from traverse ([#54](https://github.com/netease/tango/issues/54)) ([208e275](https://github.com/netease/tango/commit/208e275bb1cd98ed21d816c26179f9ebc11df223))
|
||||
- parse variables from view ([089a482](https://github.com/netease/tango/commit/089a482f750e9d9a7743a6641d6e39989347d318))
|
||||
- update types ([653386d](https://github.com/netease/tango/commit/653386dcc0b064b41915548129952eeabe53019f))
|
||||
|
||||
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-core@0.2.2...@music163/tango-core@0.2.3) (2023-11-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add onFilesChange to workspace ([#50](https://github.com/netease/tango/issues/50)) ([d775a50](https://github.com/netease/tango/commit/d775a5003ab1a14f801d3b38f7187ea15ef7d74d))
|
||||
- **core:** create store without entry file ([ae806ae](https://github.com/netease/tango/commit/ae806aee3583ef5f957836e15d715035c4556867))
|
||||
- **core:** logging drop action ([963bfe1](https://github.com/netease/tango/commit/963bfe1ab80fa4114637963534b2891a27538120))
|
||||
- designer model add defaultActiveSidebarPanel ([#51](https://github.com/netease/tango/issues/51)) ([9dd11f7](https://github.com/netease/tango/commit/9dd11f7a6d5807c80a63c84c1ad0df5bc1ce9558))
|
||||
- refactor workspace onFilesChange ([f53ce94](https://github.com/netease/tango/commit/f53ce94c6ed777d45ba6137188bfbc4566e03942))
|
||||
- update types ([46d5f59](https://github.com/netease/tango/commit/46d5f59b4cd4fa71ed247f5e470e66c8c9f6d4a0))
|
||||
|
||||
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-core@0.2.1...@music163/tango-core@0.2.2) (2023-10-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add listImportSources to viewModule ([6bbbe63](https://github.com/netease/tango/commit/6bbbe634ad52acaf06a0d776aeff450993f7b69a))
|
||||
- enhance expSetter ([#43](https://github.com/netease/tango/issues/43)) ([5ebbb42](https://github.com/netease/tango/commit/5ebbb428fb3fb786d330ab01959028443338d315))
|
||||
- update interface ([0b3e632](https://github.com/netease/tango/commit/0b3e632745e30d578268458329d5702dbd729010))
|
||||
|
||||
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-core@0.2.0...@music163/tango-core@0.2.1) (2023-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add listServiceFunctions ([09a1e21](https://github.com/netease/tango/commit/09a1e2135a1f51b0a5f6c0a507ba42d2e6355c24))
|
||||
|
||||
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-core@0.1.4...@music163/tango-core@0.2.0) (2023-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
|
||||
|
||||
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-core@0.1.3...@music163/tango-core@0.1.4) (2023-09-18)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-core@0.1.2...@music163/tango-core@0.1.3) (2023-09-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** optimize find jsxElement parent node way ([#22](https://github.com/netease/tango/issues/22)) ([19d0a45](https://github.com/netease/tango/commit/19d0a4523bb31ea9286714737a0f4f1883e1c801))
|
||||
- refactor setting formItem ([b786de2](https://github.com/netease/tango/commit/b786de2f1a0e4e9141eb09fce696e45df633b232))
|
||||
- refactor types ([15542d9](https://github.com/netease/tango/commit/15542d9eb2f8959597b81cae457091ee71710c83))
|
||||
- update sidebar ([4d809e9](https://github.com/netease/tango/commit/4d809e9afd0d6d525850708722736847b510638e))
|
||||
|
||||
## 0.1.2 (2023-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse service file with sub module ([468910a](https://github.com/netease/tango/commit/468910afde6aec75255f07f8af1f756025e1a237))
|
||||
- refactor codes ([9392231](https://github.com/netease/tango/commit/9392231414fa1f992e206804549367c5bfee52cb))
|
||||
- refactor core helpers ([f9c9cbe](https://github.com/netease/tango/commit/f9c9cbefaef7b7fa46585798834e951ded36c68a))
|
||||
- refactor designer ([ea5fb24](https://github.com/netease/tango/commit/ea5fb24ba7469e28a3de3f60597c819f2f37104a))
|
||||
@@ -0,0 +1,11 @@
|
||||
# `core`
|
||||
|
||||
搭建引擎
|
||||
|
||||
## 如何使用
|
||||
|
||||
```js
|
||||
import { createEngine } from '@music163/tango-core';
|
||||
|
||||
const engine = createEngine();
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "1.4.4",
|
||||
"description": "tango core",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
"license": "MIT",
|
||||
"main": "lib/cjs/index.js",
|
||||
"module": "lib/esm/index.js",
|
||||
"types": "lib/esm/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/netease/tango.git"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf lib/",
|
||||
"build": "yarn clean && yarn build:esm && yarn build:cjs",
|
||||
"build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020",
|
||||
"build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS",
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.25.6",
|
||||
"@babel/parser": "^7.25.6",
|
||||
"@babel/traverse": "^7.25.6",
|
||||
"@babel/types": "^7.25.6",
|
||||
"@music163/tango-helpers": "^1.2.4",
|
||||
"@types/babel__generator": "^7.6.7",
|
||||
"@types/babel__traverse": "^7.20.6",
|
||||
"mobx": "6.13.2",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mobx": "6.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
|
||||
import { AbstractWorkspace } from './models/abstract-workspace';
|
||||
|
||||
interface ICreateEngineOptions {
|
||||
/**
|
||||
* 自定义工作区
|
||||
*/
|
||||
workspace?: AbstractWorkspace;
|
||||
/**
|
||||
* 菜单信息
|
||||
*/
|
||||
menuData?: MenuDataType;
|
||||
/**
|
||||
* 默认的模拟器模式
|
||||
*/
|
||||
defaultSimulatorMode?: SimulatorNameType;
|
||||
/**
|
||||
* 默认激活的侧边栏
|
||||
*/
|
||||
defaultActiveSidebarPanel?: string;
|
||||
/**
|
||||
* 默认激活的视图
|
||||
*/
|
||||
defaultActiveView?: DesignerViewType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Designer 实例化工厂函数
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export function createEngine({
|
||||
workspace,
|
||||
defaultActiveView = 'design',
|
||||
defaultSimulatorMode = 'desktop',
|
||||
defaultActiveSidebarPanel = '',
|
||||
menuData,
|
||||
}: ICreateEngineOptions) {
|
||||
const engine = new Engine({
|
||||
workspace,
|
||||
designer: new Designer({
|
||||
workspace,
|
||||
simulator: defaultSimulatorMode,
|
||||
activeView: defaultActiveView,
|
||||
activeSidebarPanel: defaultActiveSidebarPanel,
|
||||
menuData,
|
||||
}),
|
||||
});
|
||||
|
||||
return engine;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { isValidExpressionCode } from './ast';
|
||||
|
||||
const defineServiceHandlerNames = ['defineServices', 'createServices'];
|
||||
const sfHandlerPattern = new RegExp(`^(${defineServiceHandlerNames.join('|')})$`);
|
||||
|
||||
/**
|
||||
* 判断给定的函数名是否是 defineServices
|
||||
* @param name
|
||||
* @returns
|
||||
*/
|
||||
export function isDefineService(name: string) {
|
||||
return sfHandlerPattern.test(name);
|
||||
}
|
||||
|
||||
const defineStoreHandlerName = 'defineStore';
|
||||
|
||||
/**
|
||||
* 判断给定的函数名是否是 defineStore
|
||||
* @param name
|
||||
* @returns
|
||||
*/
|
||||
export function isDefineStore(name: string) {
|
||||
return defineStoreHandlerName === name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是 tango 的变量引用
|
||||
* @example tango.stores.app.name
|
||||
* @example tango.stores?.app?.name
|
||||
*
|
||||
* @param name
|
||||
* @returns
|
||||
*/
|
||||
export function isTangoVariable(name: string) {
|
||||
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
|
||||
}
|
||||
|
||||
const templatePattern = /^{(.+)}$/;
|
||||
|
||||
/**
|
||||
* 判断给定字符串是否被表达式容器`{expCode}`包裹
|
||||
* @param code
|
||||
* @deprecated 新版改为 {{code}} 作为容器,使用 isWrappedCode 代替
|
||||
*/
|
||||
export function isWrappedByExpressionContainer(code: string, isStrict = true) {
|
||||
if (isStrict && isValidExpressionCode(code)) {
|
||||
return false;
|
||||
}
|
||||
return templatePattern.test(code);
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* ast to code
|
||||
*/
|
||||
import generator, { GeneratorOptions } from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
import { Dict, logger, wrapCode } from '@music163/tango-helpers';
|
||||
import { formatCode } from '../string';
|
||||
|
||||
const defaultGeneratorOptions: GeneratorOptions = {
|
||||
jsescOption: { minimal: true },
|
||||
retainLines: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 t.File 生成为代码
|
||||
* @param ast
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGeneratorOptions) {
|
||||
let code = generator(ast, {
|
||||
...options,
|
||||
}).code;
|
||||
code = formatCode(code);
|
||||
return code;
|
||||
}
|
||||
|
||||
const bracketPattern = /^\(.+\)$/;
|
||||
|
||||
/**
|
||||
* 将表达式生成为块级代码
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
export function expression2code(node: t.Expression) {
|
||||
const statement = t.expressionStatement(node);
|
||||
let ret = ast2code(statement).trim();
|
||||
// 移除末尾的分号
|
||||
if (ret.endsWith(';')) {
|
||||
ret = ret.slice(0, -1);
|
||||
}
|
||||
|
||||
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
|
||||
|
||||
if (isWrappingExpression && bracketPattern.test(ret)) {
|
||||
// 如果是对象,输出包含 ({}),则去掉首尾的括号
|
||||
ret = ret.slice(1, -1);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取成员表达式的调用名
|
||||
* @example Date.now() --> Date.now
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
function getNameByMemberExpression(node: t.MemberExpression | t.JSXMemberExpression): string {
|
||||
let objectName;
|
||||
let propertyName;
|
||||
|
||||
if (t.isIdentifier(node.object) || t.isJSXIdentifier(node.object)) {
|
||||
objectName = node.object.name;
|
||||
}
|
||||
|
||||
if (t.isIdentifier(node.property) || t.isJSXIdentifier(node.property)) {
|
||||
propertyName = node.property.name;
|
||||
}
|
||||
|
||||
if (t.isMemberExpression(node.object) || t.isJSXMemberExpression(node.object)) {
|
||||
objectName = getNameByMemberExpression(node.object);
|
||||
}
|
||||
|
||||
if (t.isMemberExpression(node.property) || t.isJSXMemberExpression(node.property)) {
|
||||
propertyName = getNameByMemberExpression(node.property);
|
||||
}
|
||||
|
||||
return `${objectName}.${propertyName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 的 jsxAttributeName 或 objectPropertyKey 转换为 js value
|
||||
* @param node jsxAttributeName or objectPropertyKey
|
||||
* @returns simple js value
|
||||
*/
|
||||
export function keyNode2value(node: t.Node) {
|
||||
if (!node) {
|
||||
logger.error('invalid property key', node);
|
||||
return;
|
||||
}
|
||||
|
||||
let ret;
|
||||
|
||||
switch (node.type) {
|
||||
case 'Identifier':
|
||||
case 'JSXIdentifier':
|
||||
ret = node.name;
|
||||
break;
|
||||
case 'StringLiteral':
|
||||
ret = `"${node.value}"`;
|
||||
break;
|
||||
case 'NumericLiteral':
|
||||
ret = node.value;
|
||||
break;
|
||||
case 'MemberExpression':
|
||||
ret = getNameByMemberExpression(node);
|
||||
break;
|
||||
case 'JSXMemberExpression':
|
||||
ret = getNameByMemberExpression(node);
|
||||
break;
|
||||
default:
|
||||
logger.error('unknown property key', node);
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 t.Node 生成为字符串代码
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
export function node2code(node: t.Node) {
|
||||
let ret = '';
|
||||
switch (node.type) {
|
||||
case 'StringLiteral':
|
||||
case 'NumericLiteral':
|
||||
ret = node.extra.raw as string;
|
||||
break;
|
||||
case 'BooleanLiteral':
|
||||
ret = `${node.value}`;
|
||||
break;
|
||||
case 'NullLiteral':
|
||||
ret = 'null';
|
||||
break;
|
||||
default:
|
||||
ret = expression2code(node as t.Expression);
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 t.Node 生成为 js 值
|
||||
* @param node ast node
|
||||
* @param isWrapCode 是否包裹代码,例如 code -> {{code}}
|
||||
* @returns a plain javascript value
|
||||
*/
|
||||
export function node2value(node: t.Node, isWrapCode = true): any {
|
||||
let ret;
|
||||
switch (node.type) {
|
||||
case 'StringLiteral':
|
||||
case 'NumericLiteral':
|
||||
case 'BooleanLiteral': {
|
||||
ret = node.value;
|
||||
break;
|
||||
}
|
||||
case 'NullLiteral':
|
||||
ret = null;
|
||||
break;
|
||||
case 'Identifier': // {{data}}
|
||||
case 'MemberExpression': // {{this.props.data}}
|
||||
case 'OptionalMemberExpression': // {{a?.b}}
|
||||
case 'UnaryExpression': // {{!false}}
|
||||
case 'ArrowFunctionExpression': // {{() => {}}}
|
||||
case 'TemplateLiteral': // {{`hello ${text}`}}
|
||||
case 'ConditionalExpression': // {{a ? 'foo' : 'bar'}}
|
||||
case 'LogicalExpression': // {{ a || b}}
|
||||
case 'BinaryExpression': // {{ a + b}}
|
||||
case 'TaggedTemplateExpression': // {{css``}}
|
||||
case 'CallExpression': // {{[1,2,3].map(fn)}}
|
||||
case 'JSXElement': // {{<Box>hello</Box>}}
|
||||
case 'JSXFragment': // {{<><Box /></>}}
|
||||
ret = expression2code(node);
|
||||
if (isWrapCode) {
|
||||
ret = wrapCode(ret);
|
||||
}
|
||||
break;
|
||||
case 'ObjectExpression': {
|
||||
const isSimpleObject = node.properties.every(
|
||||
(propertyNode) => propertyNode.type === 'ObjectProperty',
|
||||
);
|
||||
if (isSimpleObject) {
|
||||
// simple object: { key1, key2, key3 }
|
||||
ret = node.properties.reduce<Dict>((prev, propertyNode) => {
|
||||
if (propertyNode.type === 'ObjectProperty') {
|
||||
const key = keyNode2value(propertyNode.key);
|
||||
const value = node2value(propertyNode.value, isWrapCode);
|
||||
prev[key] = value; // key 可能是字符串,也可能是数字
|
||||
}
|
||||
return prev;
|
||||
}, {});
|
||||
} else {
|
||||
// mixed object, object property maybe SpreadElement or ObjectMethod, e.g. { key1, fn() {}, ...obj1 }
|
||||
ret = expression2code(node);
|
||||
if (wrapCode) {
|
||||
ret = wrapCode(ret);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ArrayExpression': {
|
||||
// FIXME: 有可能会解析失败
|
||||
ret = node.elements.map((elementNode) => node2value(elementNode, isWrapCode));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
logger.error('unknown ast node:', node);
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* jsx prop value 节点转为 js value
|
||||
*/
|
||||
export function jsxAttributeValueNode2value(node: t.Node): any {
|
||||
// e.g. <Checkbox checked /> 此时没有 value node
|
||||
if (!node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let ret;
|
||||
switch (node.type) {
|
||||
case 'JSXExpressionContainer':
|
||||
// <Foo bar={a}>
|
||||
// <Foo bar={a.b}>
|
||||
// <Foo bar={2.2}>
|
||||
// <Foo bar={{}}>
|
||||
// <Foo bar={[]}>
|
||||
ret = jsxAttributeValueNode2value(node.expression);
|
||||
break;
|
||||
case 'ArrayExpression': {
|
||||
// 数组统一处理为 code
|
||||
ret = expression2code(node);
|
||||
ret = wrapCode(ret);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
ret = node2value(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './generate';
|
||||
export * from './parse';
|
||||
export * from './traverse';
|
||||
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* code to ast
|
||||
*/
|
||||
import { parse, parseExpression, ParserOptions } from '@babel/parser';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
logger,
|
||||
getVariableContent,
|
||||
isPlainObject,
|
||||
Dict,
|
||||
isWrappedCode,
|
||||
getCodeOfWrappedCode,
|
||||
} from '@music163/tango-helpers';
|
||||
|
||||
// @see https://babeljs.io/docs/en/babel-parser#pluginss
|
||||
const babelParserConfig: ParserOptions = {
|
||||
sourceType: 'module',
|
||||
plugins: [
|
||||
'jsx',
|
||||
'doExpressions',
|
||||
'objectRestSpread',
|
||||
'decorators-legacy',
|
||||
'classProperties',
|
||||
'asyncGenerators',
|
||||
'functionBind',
|
||||
'dynamicImport',
|
||||
'optionalChaining',
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测代码是否是合法的代码
|
||||
* @param code
|
||||
* @returns true 为合法代码,false 为非法代码
|
||||
*/
|
||||
export function isValidCode(code: string) {
|
||||
try {
|
||||
parse(code, babelParserConfig);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测代码是否是合法的表达式代码
|
||||
* 表达式是一组代码的集合,它返回一个值;每一个合法的表达式都能计算成某个值
|
||||
* 只要你输入这段代码,可以形成一个值,就算是表达式
|
||||
* @example x = 1; // 1
|
||||
* @example 1=1
|
||||
* @example 'hello'
|
||||
* @example { foo: 'bar' }
|
||||
* @param code
|
||||
* @returns
|
||||
*/
|
||||
export function isValidExpressionCode(code: string) {
|
||||
try {
|
||||
parseExpression(code, babelParserConfig);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将源代码解析为一棵完整的 ast 树 t.File
|
||||
* @param code
|
||||
* @returns
|
||||
*/
|
||||
export function code2ast(code: string): t.File {
|
||||
return parse(code, babelParserConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将代码片段解析为 ast 节点
|
||||
* @example <Button>hello</Button>
|
||||
* @example { foo: 'foo' }
|
||||
* @example [{ foo: 'bar' }]
|
||||
* @example () => {}
|
||||
* @param code 输入字符串
|
||||
* @returns
|
||||
*/
|
||||
export function code2expression(code: string) {
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (code.endsWith(';')) {
|
||||
code = code.slice(0, -1);
|
||||
}
|
||||
|
||||
let expNode;
|
||||
try {
|
||||
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
|
||||
} catch (err) {
|
||||
logger.error('code2expression failed, invalid code:', code);
|
||||
}
|
||||
return expNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表达式代码片段转为 ast 树
|
||||
* @param code
|
||||
* @returns File
|
||||
*/
|
||||
export function expressionCode2ast(code: string) {
|
||||
const node = code2expression(code);
|
||||
return t.file(t.program([t.blockStatement([t.expressionStatement(node)])]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 js 值解析为 t.Node
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
export function value2node(
|
||||
value: any,
|
||||
):
|
||||
| t.NullLiteral
|
||||
| t.Identifier
|
||||
| t.NumericLiteral
|
||||
| t.StringLiteral
|
||||
| t.BooleanLiteral
|
||||
| t.Expression {
|
||||
let ret;
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
if (isWrappedCode(value)) {
|
||||
// 再检查是否是代码 {{code}},例如 {{this.foo}}, {{1}}
|
||||
const innerCode = getCodeOfWrappedCode(value);
|
||||
ret = code2expression(innerCode);
|
||||
} else {
|
||||
// 否则当成字符串处理
|
||||
ret = t.stringLiteral(value);
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
ret = t.numericLiteral(value);
|
||||
break;
|
||||
case 'boolean':
|
||||
ret = t.booleanLiteral(value);
|
||||
break;
|
||||
case 'function':
|
||||
ret = code2expression(String(value)) as t.ArrowFunctionExpression | t.FunctionExpression;
|
||||
break;
|
||||
case 'object': {
|
||||
if (value === null) {
|
||||
ret = t.nullLiteral();
|
||||
} else if (isPlainObject(value)) {
|
||||
ret = object2node(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
ret = t.arrayExpression(value.map((val) => value2node(val)));
|
||||
} else {
|
||||
ret = t.identifier('undefined');
|
||||
logger.error('value2node: not support value!', ret);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'undefined':
|
||||
ret = t.identifier('undefined');
|
||||
break;
|
||||
default: {
|
||||
logger.error(`value2node: value <${value}> transform failed!`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 js 普通对象解析为 t.Node
|
||||
*/
|
||||
export function object2node(
|
||||
obj: Dict,
|
||||
getValueNode: (value: any, key?: string) => t.Expression = value2node,
|
||||
) {
|
||||
if (!isPlainObject(obj)) {
|
||||
return value2node(obj);
|
||||
}
|
||||
return t.objectExpression(
|
||||
Object.keys(obj).map((key) => {
|
||||
const valNode = getValueNode(obj[key], key);
|
||||
return t.objectProperty(t.identifier(key), valNode);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function code2jsxAttributeValueNode(code: string) {
|
||||
return t.jsxExpressionContainer(code2expression(code));
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: 统一处理为 code2jsxAttributeValueNode
|
||||
* 将 js value 转为 JSXAttributeValueNode
|
||||
* @param value js value, or wrapped code
|
||||
* @returns 返回 JSXAttributeValueNode,转换失败返回Node为 {undefined}
|
||||
*/
|
||||
export function value2jsxAttributeValueNode(value: any) {
|
||||
let ret;
|
||||
switch (typeof value) {
|
||||
case 'string': {
|
||||
if (value.length > 1) {
|
||||
value = value.trim();
|
||||
}
|
||||
if (isWrappedCode(value)) {
|
||||
const innerCode = getCodeOfWrappedCode(value);
|
||||
const node = code2expression(innerCode);
|
||||
ret = t.jsxExpressionContainer(node || t.identifier('undefined'));
|
||||
} else {
|
||||
ret = t.stringLiteral(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ret = t.jsxExpressionContainer(value2node(value));
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function value2jsxChildrenValueNode(value: any) {
|
||||
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
if (isWrappedCode(value)) {
|
||||
// 代码模式
|
||||
const innerString = getVariableContent(value);
|
||||
ret = t.jsxExpressionContainer(code2expression(innerString));
|
||||
} else {
|
||||
// 普通的文本
|
||||
ret = t.jsxText(value);
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
ret = t.jsxText(String(value));
|
||||
break;
|
||||
case 'object':
|
||||
// value 为 JSXElement[]的情况下直接return
|
||||
return value as t.JSXElement[];
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ret ? [ret] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定具体的 value 值,生成 JSXAttribute
|
||||
* @param name 属性名
|
||||
* @param value 属性值代码
|
||||
* @returns
|
||||
*/
|
||||
export function makeJSXAttribute(name: string, value: any) {
|
||||
return t.jsxAttribute(t.jsxIdentifier(name), value2jsxAttributeValueNode(value));
|
||||
}
|
||||
|
||||
export function makeJSXAttributes(props: Dict) {
|
||||
return Object.keys(props).map((key) => makeJSXAttribute(key, props[key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定具体的 value 代码,生成 JSXAttribute
|
||||
* @param name 属性名
|
||||
* @param valueCode 属性值代码
|
||||
* @returns
|
||||
*/
|
||||
export function makeJSXAttributeByCode(name: string, valueCode: string) {
|
||||
return t.jsxAttribute(t.jsxIdentifier(name), code2jsxAttributeValueNode(valueCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 JSXElement
|
||||
* @param name
|
||||
* @param attributes
|
||||
* @param children
|
||||
* @param selfClosing
|
||||
* @returns
|
||||
*/
|
||||
export function makeJSXElement(
|
||||
name: string,
|
||||
attributes: t.JSXAttribute[],
|
||||
children: t.JSXElement['children'],
|
||||
selfClosing: boolean,
|
||||
) {
|
||||
return t.jsxElement(
|
||||
t.jsxOpeningElement(t.jsxIdentifier(name), attributes),
|
||||
t.jsxClosingElement(t.jsxIdentifier(name)),
|
||||
children ?? [],
|
||||
selfClosing,
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
import { getCodeOfWrappedCode, isWrappedCode } from '@music163/tango-helpers';
|
||||
import { value2node, expression2code, code2expression, node2value } from './ast';
|
||||
|
||||
/**
|
||||
* js value 转为代码字符串
|
||||
* @example 1 => 1
|
||||
* @example hello => "hello"
|
||||
* @example { foo: bar } => {{ foo: bar }}
|
||||
* @example [1,2,3] => {[1,2,3]}
|
||||
*
|
||||
* @param val js value
|
||||
* @returns 表达式代码
|
||||
*/
|
||||
export function value2code(val: any) {
|
||||
if (val === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (val === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
let ret;
|
||||
switch (typeof val) {
|
||||
case 'string': {
|
||||
if (isWrappedCode(val)) {
|
||||
ret = getCodeOfWrappedCode(val);
|
||||
} else {
|
||||
ret = `"${val}"`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'boolean':
|
||||
case 'function':
|
||||
case 'number':
|
||||
ret = String(val);
|
||||
break;
|
||||
default: {
|
||||
// other cases, including array, object, null, undefined
|
||||
const node = value2node(val);
|
||||
ret = expression2code(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export const value2expressionCode = value2code;
|
||||
|
||||
/**
|
||||
* 代码字符串转为具体的 js value
|
||||
* @example `() => {}` 返回 undefined
|
||||
*
|
||||
* @param rawCode 代码字符串
|
||||
* @returns 返回解析后的 js value,包括:string, number, boolean, simpleObject, simpleArray
|
||||
*/
|
||||
export function code2value(rawCode: string) {
|
||||
const node = code2expression(rawCode);
|
||||
const value = node2value(node);
|
||||
if (isWrappedCode(value)) {
|
||||
// 能转的就转,转不能的就返回空
|
||||
return;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { camelCase } from '@music163/tango-helpers';
|
||||
|
||||
type IdGeneratorOptionsType = { prefix?: string };
|
||||
/**
|
||||
* ID 生成器
|
||||
*/
|
||||
export class IdGenerator {
|
||||
/**
|
||||
* ID 前缀
|
||||
*/
|
||||
private readonly prefix: string;
|
||||
/**
|
||||
* 记录组件 ID 记录
|
||||
*/
|
||||
private map = new Map<string, string[]>();
|
||||
|
||||
constructor(options?: IdGeneratorOptionsType) {
|
||||
this.prefix = options?.prefix ? encodeURIComponent(options.prefix) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组件记录
|
||||
* @param component
|
||||
*/
|
||||
setItem(component: string, id?: string) {
|
||||
if (this.map.has(component)) {
|
||||
const record = this.map.get(component);
|
||||
if (id && !record.includes(id)) {
|
||||
record.push(id);
|
||||
}
|
||||
this.map.set(component, record);
|
||||
} else {
|
||||
const value = id ? [id] : [];
|
||||
this.map.set(component, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件 ID
|
||||
* @param component 组件名, 如 Button, DatePicker
|
||||
* @param codeId 用户自定义的 ID
|
||||
* @returns
|
||||
*/
|
||||
generateId(component: string, codeId?: string) {
|
||||
// FIXME: 使用 size 这里可能存在冲突的风险
|
||||
const size = this.map.get(component)?.length + 1 || 1;
|
||||
const id = codeId || `${camelCase(component)}${size}`;
|
||||
this.setItem(component, id);
|
||||
|
||||
let fullId = `${component}:${id}`;
|
||||
if (this.prefix) {
|
||||
fullId = `${this.prefix}:${fullId}`;
|
||||
}
|
||||
|
||||
return { id, fullId };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './ast';
|
||||
export * from './assert';
|
||||
export * from './code-helpers';
|
||||
export * from './string';
|
||||
export * from './object';
|
||||
export * from './prototype';
|
||||
export * from './schema-helpers';
|
||||
export * from './id-generator';
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import { IImportDeclarationPayload, IImportSpecifierSourceData } from '../types';
|
||||
|
||||
/**
|
||||
* 导入列表解析为导入声明对象
|
||||
* @param names
|
||||
* @param nameMap
|
||||
* @returns
|
||||
*/
|
||||
export function namesToImportDeclarations(
|
||||
names: string[],
|
||||
nameMap: Dict<IImportSpecifierSourceData>,
|
||||
) {
|
||||
const map: Dict = {};
|
||||
names.forEach((name) => {
|
||||
const mod = nameMap[name];
|
||||
if (mod) {
|
||||
updateMod(map, mod.source, name, mod.isDefault, !map[mod.source]);
|
||||
}
|
||||
});
|
||||
return Object.keys(map).map((sourcePath) => ({
|
||||
sourcePath,
|
||||
...map[sourcePath],
|
||||
})) as IImportDeclarationPayload[];
|
||||
}
|
||||
|
||||
function updateMod(
|
||||
map: any,
|
||||
fromPackage: string,
|
||||
specifier: string,
|
||||
isDefault = false,
|
||||
shouldInit = true,
|
||||
) {
|
||||
if (shouldInit) {
|
||||
map[fromPackage] = {};
|
||||
}
|
||||
if (isDefault) {
|
||||
map[fromPackage].defaultSpecifier = specifier;
|
||||
} else if (map[fromPackage].specifiers) {
|
||||
map[fromPackage].specifiers.push(specifier);
|
||||
} else {
|
||||
map[fromPackage].specifiers = [specifier];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
IComponentProp,
|
||||
IComponentPrototype,
|
||||
Dict,
|
||||
logger,
|
||||
uuid,
|
||||
isWrappedCode,
|
||||
getCodeOfWrappedCode,
|
||||
wrapCodeWithJSXExpressionContainer,
|
||||
} from '@music163/tango-helpers';
|
||||
import { getRelativePath, isFilepath } from './string';
|
||||
import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
|
||||
import { code2expression } from './ast';
|
||||
import { isWrappedByExpressionContainer } from './assert';
|
||||
import { value2code } from './code-helpers';
|
||||
|
||||
export function prototype2importDeclarationData(
|
||||
prototype: IComponentPrototype,
|
||||
relativeFilepath?: string,
|
||||
): { source: string; specifiers: IImportSpecifierData[] } {
|
||||
let source = prototype.package;
|
||||
const isSnippet = prototype.type === 'snippet';
|
||||
if (relativeFilepath && isFilepath(source)) {
|
||||
source = getRelativePath(relativeFilepath, source);
|
||||
}
|
||||
if (source.endsWith('.js')) {
|
||||
source = source.slice(0, -3);
|
||||
}
|
||||
|
||||
const specifiers: IImportSpecifierData[] = [];
|
||||
|
||||
if (prototype.exportType === 'defaultExport') {
|
||||
specifiers.push({
|
||||
localName: prototype.name,
|
||||
type: 'ImportDefaultSpecifier',
|
||||
});
|
||||
} else {
|
||||
// 忽略代码片段 name
|
||||
[...(isSnippet ? [] : [prototype.name]), ...(prototype.relatedImports || [])].forEach(
|
||||
(item) => {
|
||||
specifiers.push({
|
||||
localName: item,
|
||||
type: 'ImportSpecifier',
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
source,
|
||||
specifiers,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据组件的 prototype 生成 ImportDeclarationPayload
|
||||
* @deprecated
|
||||
*/
|
||||
export function getImportDeclarationPayloadByPrototype(
|
||||
prototype: IComponentPrototype,
|
||||
relativeFilepath?: string,
|
||||
): IImportDeclarationPayload {
|
||||
let defaultSpecifier;
|
||||
let specifiers;
|
||||
|
||||
if (prototype.exportType === 'defaultExport') {
|
||||
defaultSpecifier = prototype.name;
|
||||
specifiers = prototype.relatedImports || [];
|
||||
} else {
|
||||
specifiers = [...(prototype.relatedImports || [])];
|
||||
if (prototype.type !== 'snippet') {
|
||||
specifiers.push(prototype.name);
|
||||
}
|
||||
}
|
||||
|
||||
let sourcePath = prototype.package;
|
||||
if (relativeFilepath && isFilepath(sourcePath)) {
|
||||
sourcePath = getRelativePath(relativeFilepath, sourcePath);
|
||||
}
|
||||
if (sourcePath.endsWith('.js')) {
|
||||
sourcePath = sourcePath.slice(0, -3);
|
||||
}
|
||||
|
||||
return {
|
||||
defaultSpecifier,
|
||||
specifiers,
|
||||
sourcePath,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 key-value 生成 prop={value} 字符串
|
||||
* @example { name: 'foo', initValue: false } >> name={false}
|
||||
* @example { name: 'foo', initValue: 1 } >> name={1}
|
||||
* @example { name: 'foo', initValue: () => {} } >> name={()=>{}}
|
||||
* @example { name: 'foo', initValue: { foo: 'bar' } } >> name={{ foo: 'bar' }}
|
||||
* @example { name: 'foo', initValue: [{ foo: 'bar' }] } >> name={[{ foo: 'bar' }]}
|
||||
* @example { name: 'foo', initValue: 'bar' } >> name="bar"
|
||||
* @example { name: 'foo', initValue: '{() => {}}' } >> name={()=>{}}
|
||||
* @example { name: 'foo', initValue: '{{() => {}}}' } >> name={() => {}}
|
||||
* @example { name: 'foo', initValue: '{bar}' } >> name={bar}
|
||||
* @returns
|
||||
*/
|
||||
export function propDataToKeyValueString(
|
||||
item: IComponentProp,
|
||||
generateValue?: (...args: any[]) => string,
|
||||
) {
|
||||
const key = item.name;
|
||||
|
||||
let value = item.initValue;
|
||||
|
||||
if (!value && item.autoInitValue) {
|
||||
value = generateValue?.(3) || uuid(key, 3);
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
case 'function': {
|
||||
value = wrapCodeWithJSXExpressionContainer(String(value));
|
||||
break;
|
||||
}
|
||||
case 'object': {
|
||||
try {
|
||||
value = wrapCodeWithJSXExpressionContainer(value2code(value));
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'string': {
|
||||
if (isWrappedCode(value)) {
|
||||
const innerCode = getCodeOfWrappedCode(value);
|
||||
value = wrapCodeWithJSXExpressionContainer(innerCode);
|
||||
} else if (isWrappedByExpressionContainer(value)) {
|
||||
// TIP: 兼容旧版逻辑,如果是变量字符串,无需处理
|
||||
} else {
|
||||
value = `"${value}"`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return `${key}=${value}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Button prototype -> <Button>hello</Button>
|
||||
*
|
||||
* @param prototype
|
||||
* @param extraProps
|
||||
*/
|
||||
export function prototype2code(prototype: IComponentPrototype, extraProps?: Dict) {
|
||||
let code;
|
||||
switch (prototype.type) {
|
||||
case 'snippet':
|
||||
code = prototype.initChildren || prototype.defaultChildren;
|
||||
break;
|
||||
default: {
|
||||
const propList: IComponentProp[] = extraProps
|
||||
? Object.keys(extraProps).map((key) => ({
|
||||
name: key,
|
||||
initValue: extraProps[key],
|
||||
}))
|
||||
: [];
|
||||
// merge extraProps to props
|
||||
const props = [...(prototype.props || []), ...propList];
|
||||
|
||||
const keys =
|
||||
props.reduce((acc, item) => {
|
||||
const pair = propDataToKeyValueString(item, (fractionDigits: number) =>
|
||||
uuid(prototype.name, fractionDigits),
|
||||
);
|
||||
return pair ? ` ${acc} ${pair}` : acc;
|
||||
}, '') || '';
|
||||
|
||||
if (prototype.hasChildren) {
|
||||
code = `<${prototype.name} ${keys}>${
|
||||
prototype.initChildren || prototype.defaultChildren || ''
|
||||
}</${prototype.name}>`;
|
||||
} else {
|
||||
code = `<${prototype.name} ${keys.trim()} />`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 prototype 信息生成 t.JSXElement
|
||||
* @example ButtonPrototype -> <Button>hello</Button> -> t.JSXElement
|
||||
*
|
||||
* @param prototype 组件的配置信息
|
||||
* @param props 额外的属性集
|
||||
*/
|
||||
export function prototype2jsxElement(prototype: IComponentPrototype, props?: Dict) {
|
||||
const code = prototype2code(prototype, props);
|
||||
return code2expression(code) as t.JSXElement;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Dict, parseDndId, uuid } from '@music163/tango-helpers';
|
||||
|
||||
// { id: 1, component, props: { id: 1 }, children: [ { id: 2, component } ] }
|
||||
export function deepCloneNode(obj: any, component?: string): any {
|
||||
function deepCloneObject() {
|
||||
const target: Dict = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
target[key] = deepCloneNode(obj[key], component);
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
function deepCloneElementNode() {
|
||||
const target: any = deepCloneObject();
|
||||
let name = component || target.component;
|
||||
if (target.id) {
|
||||
const dnd = parseDndId(target.id);
|
||||
name = dnd.component || name;
|
||||
}
|
||||
target.id = uuid(`${name}:`);
|
||||
return target;
|
||||
}
|
||||
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => deepCloneNode(item, component));
|
||||
}
|
||||
|
||||
if (!obj.component) {
|
||||
return deepCloneObject();
|
||||
}
|
||||
|
||||
return deepCloneElementNode();
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import path from 'path';
|
||||
import { camelCase, upperCamelCase } from '@music163/tango-helpers';
|
||||
import { FileType } from './../types';
|
||||
|
||||
/**
|
||||
* 推断JS模块类型
|
||||
*/
|
||||
export function inferFileType(filename: string): FileType {
|
||||
// 增加 tangoConfigJson Module
|
||||
if (/\/tango\.config\.json$/.test(filename)) {
|
||||
return FileType.TangoConfigJsonFile;
|
||||
}
|
||||
|
||||
if (/\/appJson\.json$/.test(filename)) {
|
||||
return FileType.AppJsonFile;
|
||||
}
|
||||
|
||||
if (/\/package\.json$/.test(filename)) {
|
||||
return FileType.PackageJsonFile;
|
||||
}
|
||||
|
||||
if (/\/routes\.js$/.test(filename)) {
|
||||
return FileType.JsRouteConfigFile;
|
||||
}
|
||||
|
||||
// 所有 pages 下的 js 文件均认为是有效的 viewModule
|
||||
if (/\/pages\/.+\.jsx?$/.test(filename)) {
|
||||
return FileType.JsViewFile;
|
||||
}
|
||||
|
||||
// 所有 pages 下的 js 文件均认为是有效的 viewModule
|
||||
if (/\/pages\/.+\.schema\.json?$/.test(filename)) {
|
||||
return FileType.JsonViewFile;
|
||||
}
|
||||
|
||||
if (/\/(blocks|components)\/index\.js/.test(filename)) {
|
||||
return FileType.JsLocalComponentsEntryFile;
|
||||
}
|
||||
|
||||
if (/\/services\/.+\.js$/.test(filename)) {
|
||||
return FileType.JsServiceFile;
|
||||
}
|
||||
|
||||
if (/service\.js$/.test(filename)) {
|
||||
return FileType.JsServiceFile;
|
||||
}
|
||||
|
||||
if (/\/stores\/index\.js$/.test(filename)) {
|
||||
return FileType.JsStoreEntryFile;
|
||||
}
|
||||
|
||||
if (/\/stores\/.+\.js$/.test(filename)) {
|
||||
return FileType.JsStoreFile;
|
||||
}
|
||||
|
||||
if (/\.jsx?$/.test(filename)) {
|
||||
return FileType.JsFile;
|
||||
}
|
||||
|
||||
if (/\.json$/.test(filename)) {
|
||||
return FileType.JsonFile;
|
||||
}
|
||||
|
||||
return FileType.File;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断组件名是否合法
|
||||
* @example Button -> valid
|
||||
* @example isStrict ? div -> invalid : div -> valid
|
||||
* @param name
|
||||
* @param isStrict 是否严格模式(严格模式不匹配原生标签)
|
||||
* @returns
|
||||
*/
|
||||
export function isValidComponentName(name: string, isStrict = false) {
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isStrict) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const firstChar = name.charAt(0);
|
||||
return firstChar === firstChar.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 filename 中解析获得 moduleName
|
||||
* @example /stores/user.js -> user
|
||||
* @example /services/foo-bar.js -> fooBar
|
||||
* @param filename
|
||||
*/
|
||||
export function getModuleNameByFilename(filename: string) {
|
||||
const parts = filename.split('/');
|
||||
let name = parts[parts.length - 1];
|
||||
name = name.split('.')[0];
|
||||
return camelCase(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于路由名生成文件路径
|
||||
* @param routePath 路由地址
|
||||
* @param baseDir base dir
|
||||
* @param ext 后缀名
|
||||
* @returns
|
||||
*/
|
||||
export function getFilepath(routePath: string, baseDir: string, ext = '') {
|
||||
if (routePath.startsWith('/')) {
|
||||
routePath = routePath.substring(1);
|
||||
}
|
||||
const filename = routePath.replaceAll('/:', '@').split('/').join('-');
|
||||
if (!baseDir.endsWith('/')) {
|
||||
baseDir = `${baseDir}/`;
|
||||
}
|
||||
return `${baseDir}${filename}${ext}`;
|
||||
}
|
||||
|
||||
export function getPrivilegeCode(appName: string, routePath: string) {
|
||||
return `${appName}@${routePath.replaceAll('/', '%')}`;
|
||||
}
|
||||
|
||||
const prettier = (window as any).prettier;
|
||||
const prettierPlugins = (window as any).prettierPlugins;
|
||||
|
||||
/**
|
||||
* 格式化代码
|
||||
* @param code original source code
|
||||
* @param parser prettier parser, see https://prettier.io/docs/en/options.html#parser
|
||||
* @returns the formatted code
|
||||
*/
|
||||
export function formatCode(code: string, parser = 'babel') {
|
||||
if (prettier && prettierPlugins) {
|
||||
return prettier.format(code, { parser, plugins: prettierPlugins });
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否匹配路由
|
||||
* @example isPathnameMatchRoute('/user/123', '/users/:id') -> true
|
||||
*/
|
||||
export function isPathnameMatchRoute(pathname: string, route: string) {
|
||||
if (!pathname) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pathname = pathname.split('?')[0];
|
||||
|
||||
if (pathname === route) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const str = route.replaceAll(/:\w+/gi, '\\w+');
|
||||
const pt = new RegExp(`^${str}$`, 'i');
|
||||
return pt.test(pathname);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件路径计算区块的名字
|
||||
* @param filename 文件路径
|
||||
* @return 返回计算后的区块名(大驼峰)
|
||||
*/
|
||||
export function getBlockNameByFilename(filename: string) {
|
||||
const name = filename.split('/').slice(-2, -1)[0];
|
||||
return upperCamelCase(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并两个路径
|
||||
* @param root
|
||||
* @param filename
|
||||
* @returns
|
||||
*/
|
||||
export function getFullPath(root: string, filename: string) {
|
||||
return path.join(root, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 targetFile 在 sourceFile 中的相对引用路径
|
||||
* @param sourceFile
|
||||
* @param targetFile
|
||||
* @returns
|
||||
*/
|
||||
export function getRelativePath(sourceFile: string, targetFile: string) {
|
||||
sourceFile = path.dirname(sourceFile);
|
||||
return path.relative(sourceFile, targetFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定字符串是否是文件路径
|
||||
* @example ./pages/index.js -- yes
|
||||
* @example ../pages/index.js -- yes
|
||||
* @example ../components -- yes
|
||||
* @example /src/pages/index.js -- yes
|
||||
* @example @music163/tango-designer -- no
|
||||
* @param str
|
||||
*/
|
||||
export function isFilepath(str: string) {
|
||||
return /^(\.\.?\/|\/).*(\.[a-z]+)?$/.test(str);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './models';
|
||||
export * from './factory';
|
||||
export * from './helpers';
|
||||
export * from './types';
|
||||
|
||||
export { default as generator } from '@babel/generator';
|
||||
export { default as traverse } from '@babel/traverse';
|
||||
export * from '@babel/parser';
|
||||
@@ -0,0 +1,264 @@
|
||||
import {
|
||||
Dict,
|
||||
isStoreVariablePath,
|
||||
parseServiceVariablePath,
|
||||
parseStoreVariablePath,
|
||||
} from '@music163/tango-helpers';
|
||||
import { inferFileType, getFilepath } from '../helpers';
|
||||
import { TangoFile } from './file';
|
||||
import { FileType } from '../types';
|
||||
import { JsRouteConfigFile } from './js-route-config-file';
|
||||
import { JsStoreEntryFile } from './js-store-entry-file';
|
||||
import { JsServiceFile } from './js-service-file';
|
||||
import { JsViewFile } from './js-view-file';
|
||||
import { JsLocalComponentsEntryFile } from './js-local-components-entry-file';
|
||||
import { JsAppEntryFile } from './js-app-entry-file';
|
||||
import { JsFile } from './js-file';
|
||||
import { AbstractWorkspace, IWorkspaceInitConfig } from './abstract-workspace';
|
||||
import { JsonFile } from './json-file';
|
||||
import { JsStoreFile } from './js-store-file';
|
||||
|
||||
/**
|
||||
* CodeWorkspace 抽象基类
|
||||
*/
|
||||
export abstract class AbstractCodeWorkspace extends AbstractWorkspace {
|
||||
/**
|
||||
* 模型入口配置模块
|
||||
*/
|
||||
storeEntryModule: JsStoreEntryFile;
|
||||
|
||||
/**
|
||||
* 状态管理模块
|
||||
*/
|
||||
storeModules: Record<string, JsStoreFile>;
|
||||
|
||||
/**
|
||||
* 数据服务模块
|
||||
*/
|
||||
serviceModules: Record<string, JsServiceFile>;
|
||||
|
||||
constructor(options: IWorkspaceInitConfig) {
|
||||
super(options);
|
||||
this.storeModules = {};
|
||||
this.serviceModules = {};
|
||||
if (options?.files) {
|
||||
this.addFiles(options.files);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加文件到工作区
|
||||
* @param filename 文件名
|
||||
* @param code 代码片段
|
||||
* @param fileType 模块类型
|
||||
*/
|
||||
addFile(filename: string, code: string, fileType?: FileType) {
|
||||
if (!fileType && filename === this.entry) {
|
||||
fileType = FileType.JsAppEntryFile;
|
||||
}
|
||||
const moduleType = fileType || inferFileType(filename);
|
||||
const props = {
|
||||
filename,
|
||||
code,
|
||||
type: moduleType,
|
||||
};
|
||||
|
||||
let module;
|
||||
switch (moduleType) {
|
||||
case FileType.JsAppEntryFile:
|
||||
module = new JsAppEntryFile(this, props);
|
||||
this.jsAppEntryFile = module;
|
||||
break;
|
||||
case FileType.JsStoreEntryFile:
|
||||
module = new JsStoreEntryFile(this, props);
|
||||
this.storeEntryModule = module;
|
||||
break;
|
||||
case FileType.JsLocalComponentsEntryFile:
|
||||
module = new JsLocalComponentsEntryFile(this, props);
|
||||
this.componentsEntryModule = module;
|
||||
break;
|
||||
case FileType.JsRouteConfigFile: {
|
||||
module = new JsRouteConfigFile(this, props);
|
||||
this.routeModule = module;
|
||||
// check if activeRoute exists
|
||||
const route = module.routes.find((item) => item.path === this.activeRoute);
|
||||
if (!route) {
|
||||
this.setActiveRoute(module.routes[0]?.path);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FileType.JsViewFile:
|
||||
module = new JsViewFile(this, props);
|
||||
break;
|
||||
case FileType.JsServiceFile:
|
||||
module = new JsServiceFile(this, props);
|
||||
this.serviceModules[module.name] = module;
|
||||
break;
|
||||
case FileType.JsStoreFile:
|
||||
module = new JsStoreFile(this, props);
|
||||
this.storeModules[module.name] = module;
|
||||
break;
|
||||
case FileType.JsFile:
|
||||
module = new JsFile(this, props);
|
||||
break;
|
||||
case FileType.PackageJsonFile:
|
||||
module = new JsonFile(this, props);
|
||||
this.packageJson = module;
|
||||
break;
|
||||
case FileType.TangoConfigJsonFile:
|
||||
module = new JsonFile(this, props);
|
||||
this.tangoConfigJson = module;
|
||||
break;
|
||||
case FileType.JsonFile:
|
||||
module = new JsonFile(this, props);
|
||||
break;
|
||||
default:
|
||||
module = new TangoFile(this, props);
|
||||
}
|
||||
|
||||
this.files.set(filename, module);
|
||||
}
|
||||
|
||||
addServiceFile(serviceName: string, code: string) {
|
||||
const filename = `/src/services/${serviceName}.js`;
|
||||
this.addFile(filename, code, FileType.JsServiceFile);
|
||||
const indexServiceModule = this.serviceModules.index;
|
||||
indexServiceModule?.addImportDeclaration(`./${serviceName}`, []).update();
|
||||
}
|
||||
|
||||
addStoreFile(storeName: string, code: string) {
|
||||
const filename = `/src/stores/${storeName}.js`;
|
||||
this.addFile(filename, code);
|
||||
if (!this.storeEntryModule) {
|
||||
this.addFile('/src/stores/index.js', '');
|
||||
}
|
||||
this.storeEntryModule.addStore(storeName).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新的模型文件
|
||||
* @deprecated 使用 addStoreFile 代替
|
||||
*/
|
||||
addStoreModule(name: string, code: string) {
|
||||
this.addStoreFile(name, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型文件
|
||||
* @param name
|
||||
*/
|
||||
removeStoreModule(name: string) {
|
||||
const filename = getFilepath(name, '/src/stores', '.js');
|
||||
this.storeEntryModule.removeStore(name).update();
|
||||
this.removeFile(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加模型属性
|
||||
* @param storeName
|
||||
* @param stateName
|
||||
* @param initValue
|
||||
*/
|
||||
addStoreState(storeName: string, stateName: string, initValue: string) {
|
||||
this.storeModules[storeName]?.addState(stateName, initValue).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型属性
|
||||
* @param storeName
|
||||
* @param stateName
|
||||
*/
|
||||
removeStoreState(storeName: string, stateName: string) {
|
||||
this.storeModules[storeName]?.removeState(stateName).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变量路径删除状态变量
|
||||
* @param variablePath
|
||||
*/
|
||||
removeStoreVariable(variablePath: string) {
|
||||
const { storeName, variableName } = parseStoreVariablePath(variablePath);
|
||||
this.removeStoreState(storeName, variableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变量路径更新状态变量的值
|
||||
* @param variablePath 变量路径
|
||||
* @param code 变量代码
|
||||
*/
|
||||
updateStoreVariable(variablePath: string, code: string) {
|
||||
if (isStoreVariablePath(variablePath)) {
|
||||
const { storeName, variableName } = parseStoreVariablePath(variablePath);
|
||||
this.storeModules[storeName]?.updateState(variableName, code).update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务函数的详情
|
||||
* TODO: 不要 services 前缀
|
||||
* @param serviceKey `services.list` 或 `services.sub.list`
|
||||
* @returns
|
||||
*/
|
||||
getServiceFunction(serviceKey: string) {
|
||||
const { name, moduleName } = parseServiceVariablePath(serviceKey);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
moduleName,
|
||||
config: this.serviceModules[moduleName]?.serviceFunctions[name],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务函数的列表
|
||||
* @returns 返回服务函数的列表 { [serviceKey: string]: Dict }
|
||||
*/
|
||||
listServiceFunctions() {
|
||||
const ret: Record<string, Dict> = {};
|
||||
Object.keys(this.serviceModules).forEach((moduleName) => {
|
||||
const module = this.serviceModules[moduleName];
|
||||
Object.keys(module.serviceFunctions).forEach((name) => {
|
||||
const serviceKey = moduleName === 'index' ? name : [moduleName, name].join('.');
|
||||
ret[serviceKey] = module.serviceFunctions[name];
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新服务函数
|
||||
*/
|
||||
updateServiceFunction(serviceName: string, payload: Dict, moduleName = 'index') {
|
||||
this.serviceModules[moduleName].updateServiceFunction(serviceName, payload).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增服务函数,支持批量添加
|
||||
*/
|
||||
addServiceFunction(name: string, config: Dict, moduleName = 'index') {
|
||||
this.serviceModules[moduleName]?.addServiceFunction(name, config).update();
|
||||
}
|
||||
|
||||
addServiceFunctions(configs: Dict<Dict>, modName = 'index') {
|
||||
this.serviceModules[modName]?.addServiceFunctions(configs).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除服务函数
|
||||
* @param name
|
||||
*/
|
||||
removeServiceFunction(serviceKey: string) {
|
||||
const { moduleName, name } = parseServiceVariablePath(serviceKey);
|
||||
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新服务的基础配置
|
||||
*/
|
||||
updateServiceBaseConfig(config: Dict, moduleName = 'index') {
|
||||
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import type { FileType, IFileConfig } from '../types';
|
||||
|
||||
/**
|
||||
* 普通文件抽象基类,不进行 AST 解析
|
||||
*/
|
||||
export abstract class AbstractFile {
|
||||
readonly workspace: AbstractWorkspace;
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
readonly filename: string;
|
||||
|
||||
/**
|
||||
* 文件类型
|
||||
*/
|
||||
readonly type: FileType;
|
||||
|
||||
/**
|
||||
* 最近修改的时间戳
|
||||
*/
|
||||
lastModified: number;
|
||||
|
||||
/**
|
||||
* 文件解析是否出错
|
||||
*/
|
||||
isError: boolean;
|
||||
|
||||
/**
|
||||
* 文件解析错误消息
|
||||
*/
|
||||
errorMessage: string;
|
||||
|
||||
_code: string;
|
||||
_cleanCode: string;
|
||||
|
||||
get code() {
|
||||
return this._code;
|
||||
}
|
||||
|
||||
// FIXME: cleanCode 是不是只有 viewFile 有 ????
|
||||
get cleanCode() {
|
||||
return this._cleanCode;
|
||||
}
|
||||
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
|
||||
this.workspace = workspace;
|
||||
this.filename = props.filename;
|
||||
this.type = props.type;
|
||||
this.lastModified = Date.now();
|
||||
this.isError = false;
|
||||
|
||||
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
|
||||
if (isSyncCode) {
|
||||
this.update(props.code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文件内容
|
||||
*/
|
||||
abstract update(code?: string): void;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import * as t from '@babel/types';
|
||||
import { isNil } from '@music163/tango-helpers';
|
||||
import {
|
||||
code2ast,
|
||||
ast2code,
|
||||
formatCode,
|
||||
traverseFile,
|
||||
addImportDeclaration,
|
||||
updateImportDeclaration,
|
||||
} from '../helpers';
|
||||
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractFile } from './abstract-file';
|
||||
|
||||
/**
|
||||
* JS 文件抽象基类
|
||||
* - ast 操纵类方法,统一返回 this,支持外层链式调用
|
||||
* - observable state 统一用 _foo 格式,并提供 getter 方法
|
||||
*/
|
||||
export abstract class AbstractJsFile extends AbstractFile {
|
||||
ast: t.File;
|
||||
|
||||
/**
|
||||
* ast 是否与 code 保持同步
|
||||
*/
|
||||
isAstSynced: boolean;
|
||||
|
||||
/**
|
||||
* 导入的依赖列表
|
||||
*/
|
||||
importList: ImportDeclarationDataType;
|
||||
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
|
||||
super(workspace, props, isSyncCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于最新的 ast 进行同步
|
||||
* @param code 如果传入 code,则基于 code 进行同步
|
||||
* @param isSyncAst 是否同步 ast
|
||||
* @param isRefreshWorkspace 是否刷新 workspace
|
||||
*/
|
||||
update(code?: string, isSyncAst = true, isRefreshWorkspace = true) {
|
||||
this.lastModified = Date.now();
|
||||
|
||||
try {
|
||||
if (isNil(code)) {
|
||||
this._syncByAst();
|
||||
} else {
|
||||
this._syncByCode(code, isSyncAst);
|
||||
}
|
||||
|
||||
if (isSyncAst) {
|
||||
this._analysisAst();
|
||||
}
|
||||
|
||||
this.isAstSynced = isSyncAst;
|
||||
this.isError = false;
|
||||
this.errorMessage = undefined;
|
||||
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
if (isRefreshWorkspace) {
|
||||
this.workspace.refresh([this.filename]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.isError = true;
|
||||
this.errorMessage = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于当前的代码重新生成 ast
|
||||
*/
|
||||
updateAst() {
|
||||
if (!this.isAstSynced) {
|
||||
try {
|
||||
this.ast = code2ast(this._code);
|
||||
this._analysisAst();
|
||||
this.isAstSynced = true;
|
||||
this.isError = false;
|
||||
this.errorMessage = undefined;
|
||||
} catch (err: any) {
|
||||
this.isAstSynced = false;
|
||||
this.isError = true;
|
||||
this.errorMessage = err.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
|
||||
this.ast = addImportDeclaration(this.ast, source, specifiers);
|
||||
return this;
|
||||
}
|
||||
|
||||
updateImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
|
||||
this.ast = updateImportDeclaration(this.ast, source, specifiers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于最新的 ast 进行源码同步
|
||||
*/
|
||||
_syncByAst() {
|
||||
const code = ast2code(this.ast);
|
||||
this._code = code;
|
||||
this._cleanCode = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于输入的源码进行同步
|
||||
* @param code 源码
|
||||
* @param isSyncAst 是否同步 ast
|
||||
* @returns
|
||||
*/
|
||||
_syncByCode(code: string, isSyncAst = true) {
|
||||
if (code === this._code) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 提前格式化代码
|
||||
try {
|
||||
code = formatCode(code);
|
||||
} catch (err) {
|
||||
// err ignored, format code failed
|
||||
}
|
||||
|
||||
this._code = code;
|
||||
this._cleanCode = code;
|
||||
if (isSyncAst) {
|
||||
this.ast = code2ast(code);
|
||||
}
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
const { imports } = traverseFile(this.ast);
|
||||
this.importList = imports;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
|
||||
import type { IFileConfig } from '../types';
|
||||
import { formatCode } from '../helpers';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractFile } from './abstract-file';
|
||||
|
||||
export abstract class AbstractJsonFile extends AbstractFile {
|
||||
_object;
|
||||
|
||||
abstract get json(): object;
|
||||
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this._object = {};
|
||||
this.update(props.code);
|
||||
}
|
||||
|
||||
update(code?: string) {
|
||||
this.lastModified = Date.now();
|
||||
|
||||
if (isNil(code)) {
|
||||
// 基于最新的 json 同步代码
|
||||
let newCode = JSON.stringify(this._object);
|
||||
try {
|
||||
newCode = formatCode(newCode, 'json');
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
this._code = newCode;
|
||||
this._cleanCode = newCode;
|
||||
} else {
|
||||
try {
|
||||
// 基于传入的代码,同步 json 对象
|
||||
code = formatCode(code, 'json');
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
this._code = code;
|
||||
this._cleanCode = code;
|
||||
try {
|
||||
const json = JSON.parse(code);
|
||||
this._object = json;
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径取值
|
||||
* @param valuePath
|
||||
* @returns
|
||||
*/
|
||||
getValue(valuePath: string) {
|
||||
return getValue(this.json, valuePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径设置值
|
||||
* @param valuePath
|
||||
* @param visitor
|
||||
*/
|
||||
setValue(valuePath: string, visitor: (targetValue: any) => any) {
|
||||
const target = this.getValue(valuePath);
|
||||
let next: unknown;
|
||||
if (typeof visitor === 'function') {
|
||||
next = visitor?.(target);
|
||||
} else {
|
||||
next = visitor;
|
||||
}
|
||||
if (next !== undefined) {
|
||||
setValue(this._object, valuePath, next);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径删除值
|
||||
* @param valuePath
|
||||
* @param visitor
|
||||
*/
|
||||
deleteValue(valuePath: string) {
|
||||
const pathList = valuePath.split('.');
|
||||
const lastPath = pathList.pop();
|
||||
const parentPath = pathList.join('.');
|
||||
let target;
|
||||
if (parentPath) {
|
||||
target = this.getValue(parentPath);
|
||||
} else {
|
||||
target = this.json;
|
||||
}
|
||||
if (!target) {
|
||||
return this;
|
||||
}
|
||||
delete target[lastPath];
|
||||
if (parentPath) {
|
||||
this.setValue(parentPath, target);
|
||||
} else {
|
||||
this._object = target;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import { AbstractFile } from './abstract-file';
|
||||
|
||||
export interface IViewNodeInitConfig<RawNodeType = unknown, ViewFileType = AbstractFile> {
|
||||
id: string;
|
||||
component: string;
|
||||
rawNode: RawNodeType;
|
||||
file: ViewFileType;
|
||||
}
|
||||
|
||||
export abstract class AbstractViewNode<RawNodeType = unknown, ViewFileType = AbstractFile> {
|
||||
/**
|
||||
* 节点 ID
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* 节点对应的组件名
|
||||
*/
|
||||
readonly component: string;
|
||||
|
||||
readonly rawNode: RawNodeType;
|
||||
|
||||
/**
|
||||
* 节点所属的文件对象
|
||||
*/
|
||||
file: ViewFileType;
|
||||
|
||||
/**
|
||||
* 节点所属的文件对象
|
||||
*/
|
||||
props: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 节点的位置信息
|
||||
*/
|
||||
abstract get loc(): unknown;
|
||||
|
||||
constructor(props: IViewNodeInitConfig<RawNodeType, ViewFileType>) {
|
||||
this.id = props.id;
|
||||
this.component = props.component;
|
||||
this.rawNode = props.rawNode;
|
||||
this.file = props.file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁当前节点,清空文件和节点的关联关系
|
||||
*/
|
||||
destroy() {
|
||||
this.file = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回克隆后的 ast 节点
|
||||
* @param overrideProps 额外设置给克隆节点的属性
|
||||
* @returns 返回克隆的原始节点
|
||||
*/
|
||||
abstract cloneRawNode(overrideProps?: Dict): RawNodeType;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,308 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export type SimulatorNameType = 'desktop' | 'phone';
|
||||
|
||||
export type DesignerViewType = 'design' | 'code' | 'dual';
|
||||
|
||||
interface ISimulatorType {
|
||||
name: SimulatorNameType;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface IViewportBounding {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface IDesignerOptions {
|
||||
workspace: AbstractWorkspace;
|
||||
simulator?: SimulatorNameType | ISimulatorType;
|
||||
/**
|
||||
* 菜单配置
|
||||
*/
|
||||
menuData: MenuDataType;
|
||||
activeSidebarPanel?: string;
|
||||
/**
|
||||
* 默认激活的视图模式
|
||||
*/
|
||||
activeView?: DesignerViewType;
|
||||
}
|
||||
|
||||
const ISimulatorTypes: Record<string, ISimulatorType> = {
|
||||
desktop: {
|
||||
name: 'desktop',
|
||||
width: 1366,
|
||||
height: 800,
|
||||
},
|
||||
phone: {
|
||||
name: 'phone',
|
||||
width: 375,
|
||||
height: 812,
|
||||
},
|
||||
};
|
||||
|
||||
export class Designer {
|
||||
/**
|
||||
* 当前的沙箱模拟器类型
|
||||
*/
|
||||
_simulator: ISimulatorType = ISimulatorTypes.desktop;
|
||||
|
||||
/**
|
||||
* 当前的视图尺寸
|
||||
*/
|
||||
_viewport: IViewportBounding = {
|
||||
width: 1366,
|
||||
height: 800,
|
||||
};
|
||||
|
||||
/**
|
||||
* 当前激活的视图
|
||||
*/
|
||||
_activeView: DesignerViewType = 'design';
|
||||
|
||||
/**
|
||||
* 当前选中的侧边栏面板
|
||||
*/
|
||||
_activeSidebarPanel = '';
|
||||
|
||||
/**
|
||||
* 是否显示智能引导
|
||||
*/
|
||||
_showSmartWizard = false;
|
||||
|
||||
/**
|
||||
* 是否显示添加组件面板
|
||||
*/
|
||||
_showAddComponentPopover = false;
|
||||
|
||||
/**
|
||||
* 添加组件面板的位置
|
||||
*/
|
||||
_addComponentPopoverPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
/**
|
||||
* 是否显示右键菜单
|
||||
*/
|
||||
_showContextMenu = false;
|
||||
|
||||
/**
|
||||
* 右键菜单在 iframe 上的位置
|
||||
*/
|
||||
_contextMenuPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
/**
|
||||
* 是否显示右侧面板
|
||||
*/
|
||||
_showRightPanel = true;
|
||||
|
||||
/**
|
||||
* 是否预览模式
|
||||
*/
|
||||
_isPreview = false;
|
||||
|
||||
/**
|
||||
* 菜单列表
|
||||
*/
|
||||
_menuData?: MenuDataType = null;
|
||||
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get simulator(): ISimulatorType {
|
||||
return toJS(this._simulator);
|
||||
}
|
||||
|
||||
get viewport() {
|
||||
return toJS(this._viewport);
|
||||
}
|
||||
|
||||
get activeView() {
|
||||
return this._activeView;
|
||||
}
|
||||
|
||||
get isPreview() {
|
||||
return this._isPreview;
|
||||
}
|
||||
|
||||
get showSmartWizard() {
|
||||
return this._showSmartWizard;
|
||||
}
|
||||
|
||||
get activeSidebarPanel() {
|
||||
return this._activeSidebarPanel;
|
||||
}
|
||||
|
||||
get showRightPanel() {
|
||||
return this._showRightPanel;
|
||||
}
|
||||
|
||||
get showAddComponentPopover() {
|
||||
return this._showAddComponentPopover;
|
||||
}
|
||||
|
||||
get addComponentPopoverPosition() {
|
||||
return this._addComponentPopoverPosition;
|
||||
}
|
||||
|
||||
get showContextMenu() {
|
||||
return this._showContextMenu;
|
||||
}
|
||||
|
||||
get contextMenuPosition() {
|
||||
return this._contextMenuPosition;
|
||||
}
|
||||
|
||||
get menuData() {
|
||||
return this._menuData ?? ([] as MenuDataType);
|
||||
}
|
||||
|
||||
constructor(options: IDesignerOptions) {
|
||||
this.workspace = options.workspace;
|
||||
|
||||
const {
|
||||
simulator,
|
||||
menuData,
|
||||
activeSidebarPanel: defaultActiveSidebarPanel,
|
||||
activeView: defaultActiveView,
|
||||
} = options;
|
||||
|
||||
if (menuData) {
|
||||
this.setMenuData(menuData);
|
||||
}
|
||||
|
||||
// 默认设计器模式
|
||||
if (simulator) {
|
||||
this.setSimulator(simulator);
|
||||
}
|
||||
|
||||
// 默认展开的侧边栏
|
||||
if (defaultActiveSidebarPanel) {
|
||||
this.setActiveSidebarPanel(defaultActiveSidebarPanel);
|
||||
}
|
||||
|
||||
// 默认激活的视图
|
||||
if (defaultActiveView) {
|
||||
this.setActiveView(defaultActiveView);
|
||||
}
|
||||
|
||||
makeObservable(this, {
|
||||
_simulator: observable,
|
||||
_viewport: observable,
|
||||
_activeView: observable,
|
||||
_activeSidebarPanel: observable,
|
||||
_showSmartWizard: observable,
|
||||
_showRightPanel: observable,
|
||||
_showAddComponentPopover: observable,
|
||||
_addComponentPopoverPosition: observable,
|
||||
_showContextMenu: observable,
|
||||
_contextMenuPosition: observable,
|
||||
_menuData: observable,
|
||||
_isPreview: observable,
|
||||
simulator: computed,
|
||||
viewport: computed,
|
||||
activeView: computed,
|
||||
activeSidebarPanel: computed,
|
||||
isPreview: computed,
|
||||
showRightPanel: computed,
|
||||
showSmartWizard: computed,
|
||||
showAddComponentPopover: computed,
|
||||
addComponentPopoverPosition: computed,
|
||||
showContextMenu: computed,
|
||||
contextMenuPosition: computed,
|
||||
menuData: computed,
|
||||
setSimulator: action,
|
||||
setViewport: action,
|
||||
setActiveView: action,
|
||||
setActiveSidebarPanel: action,
|
||||
closeSidebarPanel: action,
|
||||
toggleRightPanel: action,
|
||||
toggleSmartWizard: action,
|
||||
toggleIsPreview: action,
|
||||
toggleAddComponentPopover: action,
|
||||
toggleContextMenu: action,
|
||||
});
|
||||
}
|
||||
|
||||
setSimulator(value: ISimulatorType | SimulatorNameType) {
|
||||
if (typeof value === 'string') {
|
||||
this._simulator = ISimulatorTypes[value];
|
||||
} else {
|
||||
this._simulator = value;
|
||||
}
|
||||
}
|
||||
|
||||
setViewport(value: IViewportBounding) {
|
||||
this._viewport = value;
|
||||
}
|
||||
|
||||
setActiveView(view: DesignerViewType) {
|
||||
this._activeView = view;
|
||||
if (view === 'dual') {
|
||||
this.setActiveSidebarPanel('');
|
||||
this.toggleRightPanel(false);
|
||||
this.toggleIsPreview(true);
|
||||
} else if (view === 'design') {
|
||||
this.toggleIsPreview(false);
|
||||
}
|
||||
}
|
||||
|
||||
setActiveSidebarPanel(panel: string) {
|
||||
if (panel && panel !== this.activeSidebarPanel) {
|
||||
this._activeSidebarPanel = panel;
|
||||
} else {
|
||||
this._activeSidebarPanel = '';
|
||||
}
|
||||
}
|
||||
|
||||
setMenuData(menuData: MenuDataType) {
|
||||
this._menuData = menuData;
|
||||
}
|
||||
|
||||
closeSidebarPanel() {
|
||||
this._activeSidebarPanel = '';
|
||||
}
|
||||
|
||||
toggleSmartWizard(value: boolean) {
|
||||
this._showSmartWizard = value;
|
||||
}
|
||||
|
||||
toggleRightPanel(value?: boolean) {
|
||||
this._showRightPanel = value ?? !this._showRightPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示添加组件面板
|
||||
* @param value 是否显示
|
||||
* @param position 坐标
|
||||
*/
|
||||
toggleAddComponentPopover(
|
||||
value: boolean,
|
||||
position: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
} = this.addComponentPopoverPosition,
|
||||
) {
|
||||
this._showAddComponentPopover = value;
|
||||
this._addComponentPopoverPosition = position;
|
||||
}
|
||||
|
||||
toggleIsPreview(value: boolean) {
|
||||
this._isPreview = value ?? !this._isPreview;
|
||||
if (value) {
|
||||
this.workspace.selectSource.clear();
|
||||
}
|
||||
}
|
||||
|
||||
toggleContextMenu(
|
||||
value: boolean,
|
||||
position: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
} = this.contextMenuPosition,
|
||||
) {
|
||||
this._showContextMenu = value;
|
||||
this._contextMenuPosition = position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { ISelectedItemData } from '@music163/tango-helpers';
|
||||
import { DropTarget } from './drop-target';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
/**
|
||||
* 拖拽来源类,被拖拽的物体
|
||||
*/
|
||||
export class DragSource {
|
||||
/**
|
||||
* 是否处于拖拽状态
|
||||
*/
|
||||
isDragging: boolean;
|
||||
|
||||
/**
|
||||
* 选中的目标元素数据
|
||||
*/
|
||||
data: ISelectedItemData;
|
||||
|
||||
/**
|
||||
* 放置目标
|
||||
*/
|
||||
dropTarget: DropTarget;
|
||||
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get node() {
|
||||
return this.workspace.getNode(this.data?.id, this.data?.filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应的 prototype
|
||||
*/
|
||||
get prototype() {
|
||||
return this.workspace.getPrototype(this.data?.name);
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.data?.id;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.data?.name;
|
||||
}
|
||||
|
||||
get bounding() {
|
||||
return this.data?.bounding;
|
||||
}
|
||||
|
||||
constructor(workspace: AbstractWorkspace) {
|
||||
this.workspace = workspace;
|
||||
this.data = null;
|
||||
this.isDragging = false;
|
||||
this.dropTarget = new DropTarget(workspace);
|
||||
|
||||
makeObservable(this, {
|
||||
data: observable,
|
||||
isDragging: observable,
|
||||
set: action,
|
||||
clear: action,
|
||||
node: computed,
|
||||
prototype: computed,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新选中数据
|
||||
* @param props
|
||||
*/
|
||||
set(data: ISelectedItemData) {
|
||||
this.data = data;
|
||||
this.isDragging = !!data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
clear() {
|
||||
this.data = null;
|
||||
this.isDragging = false;
|
||||
this.dropTarget.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应的 node
|
||||
* @deprecated
|
||||
*/
|
||||
getNode() {
|
||||
return this.node;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { ISelectedItemData } from '@music163/tango-helpers';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export enum DropMethod {
|
||||
ReplaceNode = 'replaceNode', // 替换节点
|
||||
InsertBefore = 'insertBefore', // 插入节点,放置在前面
|
||||
InsertAfter = 'insertAfter', // 插入节点,放置在后面
|
||||
InsertChild = 'insertChild', // 插入子节点,放置在最后
|
||||
InsertFirstChild = 'insertFirstChild', // 插入子节点,放置在最前
|
||||
}
|
||||
|
||||
/**
|
||||
* 放置目标类
|
||||
*/
|
||||
export class DropTarget {
|
||||
/**
|
||||
* 插入方法
|
||||
*/
|
||||
method: DropMethod;
|
||||
/**
|
||||
* 放置的目标元素数据
|
||||
*/
|
||||
data: ISelectedItemData;
|
||||
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get node() {
|
||||
return this.workspace.getNode(this.data.id, this.data.filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应的 prototype
|
||||
*/
|
||||
get prototype() {
|
||||
return this.data?.name ? this.workspace.getPrototype(this.data?.name) : null;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.data?.id;
|
||||
}
|
||||
|
||||
get bounding() {
|
||||
return this.data?.bounding;
|
||||
}
|
||||
|
||||
get display() {
|
||||
return this.data?.display;
|
||||
}
|
||||
|
||||
constructor(workspace: AbstractWorkspace) {
|
||||
this.workspace = workspace;
|
||||
this.method = DropMethod.InsertAfter;
|
||||
this.data = null;
|
||||
|
||||
makeObservable(this, {
|
||||
method: observable,
|
||||
data: observable,
|
||||
set: action,
|
||||
clear: action,
|
||||
node: computed,
|
||||
});
|
||||
}
|
||||
|
||||
set(data: ISelectedItemData, method: DropMethod) {
|
||||
this.data = data;
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
clear() {
|
||||
this.data = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应的 node
|
||||
* @deprecated
|
||||
*/
|
||||
getNode() {
|
||||
return this.node;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { Designer } from './designer';
|
||||
|
||||
/**
|
||||
* 设计器引擎
|
||||
*/
|
||||
export class Engine {
|
||||
/**
|
||||
* 工作区状态
|
||||
*/
|
||||
workspace: AbstractWorkspace;
|
||||
/**
|
||||
* 设计器状态
|
||||
*/
|
||||
designer: Designer;
|
||||
|
||||
constructor(options: Pick<Engine, 'workspace' | 'designer'>) {
|
||||
this.workspace = options.workspace;
|
||||
this.designer = options.designer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { isNil } from '@music163/tango-helpers';
|
||||
import type { IFileConfig } from '../types';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractFile } from './abstract-file';
|
||||
|
||||
export class TangoFile extends AbstractFile {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
});
|
||||
}
|
||||
|
||||
update(code?: string) {
|
||||
if (!isNil(code)) {
|
||||
this.lastModified = Date.now();
|
||||
this._code = code;
|
||||
this._cleanCode = code;
|
||||
}
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export enum HistoryMessage {
|
||||
InitView = 'initView',
|
||||
AddFile = 'addFile',
|
||||
RemoveFile = 'removeFile',
|
||||
UpdateDependency = 'updateDependency',
|
||||
RemoveDependency = 'removeDependency',
|
||||
RemoveNode = 'removeNode',
|
||||
ReplaceNode = 'replaceNode',
|
||||
CloneNode = 'cloneNode',
|
||||
InsertNode = 'insertNode',
|
||||
InsertBeforeNode = 'insertBeforeNode',
|
||||
InsertAfterNode = 'insertAfterNode',
|
||||
DropNode = 'dropNode',
|
||||
UpdateAttribute = 'updateAttribute',
|
||||
UpdateCode = 'updateCode',
|
||||
}
|
||||
|
||||
type HistoryRecordData = {
|
||||
[filename: string]: string;
|
||||
};
|
||||
|
||||
interface HistoryRecord {
|
||||
time: number;
|
||||
message: HistoryMessage;
|
||||
data: HistoryRecordData;
|
||||
}
|
||||
|
||||
type PushDataType = Pick<HistoryRecord, 'message' | 'data'>;
|
||||
|
||||
/**
|
||||
* 工作区的历史记录记录
|
||||
*/
|
||||
export class TangoHistory {
|
||||
// 历史记录
|
||||
_records: HistoryRecord[] = [];
|
||||
|
||||
// 当前记录指针
|
||||
_index = 0;
|
||||
|
||||
// 最多记录数
|
||||
_maxSize = 100;
|
||||
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get index() {
|
||||
return this._index;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this._records.length;
|
||||
}
|
||||
|
||||
get list() {
|
||||
return toJS(this._records);
|
||||
}
|
||||
|
||||
get couldBack() {
|
||||
return this._records.length > 0 && this._index > 0;
|
||||
}
|
||||
|
||||
get couldForward() {
|
||||
return this._records.length > this._index + 1;
|
||||
}
|
||||
|
||||
constructor(workspace: AbstractWorkspace) {
|
||||
this.workspace = workspace;
|
||||
|
||||
makeObservable(this, {
|
||||
_records: observable,
|
||||
_index: observable,
|
||||
back: action,
|
||||
forward: action,
|
||||
go: action,
|
||||
push: action,
|
||||
couldBack: computed,
|
||||
couldForward: computed,
|
||||
});
|
||||
}
|
||||
|
||||
_sync(data: HistoryRecordData) {
|
||||
if (data) {
|
||||
Object.keys(data).forEach((filename) => {
|
||||
this.workspace.getFile(filename).update(data[filename]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上一步
|
||||
*/
|
||||
back() {
|
||||
if (this.couldBack) {
|
||||
const item = this._records[this._index - 1];
|
||||
this._sync(item.data);
|
||||
this._index--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下一步
|
||||
*/
|
||||
forward() {
|
||||
if (this.couldForward) {
|
||||
const item = this._records[this._index + 1];
|
||||
this._sync(item.data);
|
||||
this._index++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过相对位置从历史记录加载记录
|
||||
*/
|
||||
go(index: number) {
|
||||
const item = this._records[index];
|
||||
if (item) {
|
||||
this._sync(item.data);
|
||||
this._index = index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* push 数据进入历史记录堆栈
|
||||
*/
|
||||
push(data: PushDataType) {
|
||||
if (this._index < this._records.length - 1) {
|
||||
this._records = this._records.slice(0, this._index + 1);
|
||||
}
|
||||
|
||||
this._index = this._records.length;
|
||||
this._records.push({
|
||||
time: Date.now(),
|
||||
...data,
|
||||
});
|
||||
|
||||
const overCount = this._records.length - this._maxSize;
|
||||
if (overCount > 0) {
|
||||
this._records.splice(0, overCount);
|
||||
this._index = this._records.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export * from './abstract-workspace';
|
||||
export * from './abstract-code-workspace';
|
||||
export * from './abstract-file';
|
||||
export * from './abstract-js-file';
|
||||
export * from './abstract-json-file';
|
||||
export * from './abstract-view-node';
|
||||
export * from './designer';
|
||||
export * from './drag-source';
|
||||
export * from './drop-target';
|
||||
export * from './engine';
|
||||
export * from './file';
|
||||
export * from './history';
|
||||
export * from './interfaces';
|
||||
export * from './js-app-entry-file';
|
||||
export * from './js-file';
|
||||
export * from './js-local-components-entry-file';
|
||||
export * from './js-route-config-file';
|
||||
export * from './js-service-file';
|
||||
export * from './js-store-entry-file';
|
||||
export * from './js-store-file';
|
||||
export * from './js-view-file';
|
||||
export * from './json-file';
|
||||
export * from './select-source';
|
||||
export * from './view-node';
|
||||
export * from './workspace';
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import {
|
||||
InsertChildPositionType,
|
||||
IImportSpecifierSourceData,
|
||||
IImportSpecifierData,
|
||||
} from '../types';
|
||||
import { IdGenerator } from '../helpers';
|
||||
import { AbstractViewNode } from './abstract-view-node';
|
||||
|
||||
export interface IViewFile {
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
filename: string;
|
||||
|
||||
/**
|
||||
* ID 生成器
|
||||
*/
|
||||
idGenerator: IdGenerator;
|
||||
|
||||
/**
|
||||
* 通过导入组件名查找组件来自的包
|
||||
*/
|
||||
importMap?: Dict<IImportSpecifierSourceData>;
|
||||
|
||||
/**
|
||||
* 判断节点是否存在
|
||||
* @param codeId 节点 ID
|
||||
* @returns 存在返回 true,否则返回 false
|
||||
*/
|
||||
hasNodeByCodeId?: (codeId: string) => boolean;
|
||||
|
||||
listImportSources?: () => string[];
|
||||
listModals?: () => Array<{ label: string; value: string }>;
|
||||
listForms?: () => Record<string, string[]>;
|
||||
|
||||
update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void;
|
||||
|
||||
/**
|
||||
* 添加新的导入符号
|
||||
* @param source 导入来源
|
||||
* @param newSpecifiers 新导入符号列表
|
||||
* @returns this
|
||||
*/
|
||||
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
|
||||
|
||||
getNode: (targetNodeId: string) => AbstractViewNode;
|
||||
|
||||
removeNode: (targetNodeId: string) => this;
|
||||
|
||||
insertChild: (targetNodeId: string, newNode: any, position?: InsertChildPositionType) => this;
|
||||
|
||||
insertAfter: (targetNodeId: string, newNode: any) => this;
|
||||
|
||||
insertBefore: (targetNodeId: string, newNode: any) => this;
|
||||
|
||||
replaceNode: (targetNodeId: string, newNode: any) => this;
|
||||
|
||||
replaceViewChildren: (rawNodes: any[]) => this;
|
||||
|
||||
updateNodeAttribute: (
|
||||
nodeId: string,
|
||||
attrName: string,
|
||||
attrValue?: any,
|
||||
relatedImports?: string[],
|
||||
) => this;
|
||||
|
||||
updateNodeAttributes: (
|
||||
nodeId: string,
|
||||
config: Record<string, any>,
|
||||
relatedImports?: string[],
|
||||
) => this;
|
||||
|
||||
get nodes(): Map<string, AbstractViewNode>;
|
||||
get nodesTree(): object[];
|
||||
get tree(): any;
|
||||
/**
|
||||
* 文件中的代码
|
||||
*/
|
||||
get code(): string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { traverseEntryFile } from '../helpers';
|
||||
import { IFileConfig } from '../types';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export class JsAppEntryFile extends AbstractJsFile {
|
||||
routerType: string;
|
||||
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
const config = traverseEntryFile(this.ast);
|
||||
this.routerType = config?.router?.type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { IFileConfig } from '../types';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* 普通 JS 文件
|
||||
*/
|
||||
export class JsFile extends AbstractJsFile {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import path from 'path';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { IExportSpecifierData, IFileConfig } from '../types';
|
||||
import { traverseComponentsEntryFile } from '../helpers';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* 本地组件目录的入口文件,例如 '/components/index.js' 或 `/blocks/index.js`
|
||||
*/
|
||||
export class JsLocalComponentsEntryFile extends AbstractJsFile {
|
||||
exportList: Record<string, IExportSpecifierData>;
|
||||
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
exportList: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
});
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
const baseDir = path.dirname(this.filename);
|
||||
const { exportMap } = traverseComponentsEntryFile(this.ast, baseDir);
|
||||
this.exportList = exportMap;
|
||||
Object.keys(this.exportList).forEach((key) => {
|
||||
this.workspace.componentPrototypes.set(key, {
|
||||
name: key,
|
||||
exportType: 'namedExport',
|
||||
package: baseDir,
|
||||
type: 'element',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user