Compare commits

..

1 Commits

Author SHA1 Message Date
GaoNeng dbb8365f35
feat: 花岗岩小队作品提交 (#414)
* fix:修复资源管理工具类型切换 (#239)

* fix:修复资源管理工具类型切换

* fix: 左右两侧面板宽度固定为280px (#230)

* fix(styles-spacing): 增大物料设置内外边距时点击区域 #134 (#184)

* fix(styles-spacing): 增大物料设置内外边距时点击区域 #134

* fix:预览引入builtin组件依赖,built组件样式文件注入js (#251)

* add config (#262)

* add config

* update config

* fix: 修复由于物料边框引起的左侧面板样式问题 (#247)

* fix: 设计器构建后使用vue runtime prod build(本地开发保持development, preview预览保持development) (#254)

* fix(canvas-renderer): 修复 isNative component 判断失效的 bug (#241)

* feat: js panel add jsx support  (#253)

* feat: JS面板支持JSX语法

* fix: 画布解析JS表达式时支持JSX语法

* fix: 修复画布解析JSX表达式时会返回undefined

* feat: 添加Tree和Tooltip的自定义渲染函数属性

* fix: 修改变量名

* feat: algo node

* fix: 修复报错

* fix: fix overflow

* feat: add node

* feat(setting): property

* feat: property setting

* feat(layer): bundle schema

* feat: useMonaco hook

* feat(code): monaco editor

* feat: code editor

* refactor: monaco editor

* feat: useVisitor

* feat(front-end): save layer

* init: backend project init

* refactor: move dl-flow-front repository to dl-flow

* chore: move dl-flow-backend to dl-flow

* chore: 重构仓库结构

* ci: fix ci

* feat(backend): unit test

* chore: update ci

* ci: 手动触发

* fix: ci error

* feat: hot load & minifiy

* fix: ci error

* feat(back): refactor to nest.js

* feat: expose port in docker-compose.yaml

* release: v0.0.2

* release: v0.0.3 release

* release: v0.0.4

* fix: ci fix

* fix: ci

* fix: ci

* fix: ci

* fix: ci

* fix: ci

* fix: ci

* fix: fix import

* fix: fix import

* feat(backend): layer controller

* feat(bakend): material

* fix: fix layer unit test error

* release: version 0.0.7 release

* fix(front-end): fix warning

* feat: save layer

* fix(backend): if property is undefined will no longer throw server error

* feat: remove memory server in dev env

* fix(front): delete tab item lazy load

fix(front): label i18n

* feat: remove cache when install dependencies

* fix: ci

* styled(front): delete console.log

* feat(frontend): context menu

* feat(front): group

* feat(front): group layout

* feat(front): group auto size & auto position

* feat(front): update schema

* feat(front): schema plugin

* feat(front): schema editor

* feat(front): schema field change

* feat(front): schema dynamic change

* feat(frontend): export button

* feat(backend): should add layer identifier when save

* feat(backend): code-generate service

* feat(backend): code generate

* test(backend): base unit test

* test(ast): ast base unit test

* feat(frontend): emit createCodeGenerate event

* feat(bakend): generate python code

* feat(backend): file download

* feat(front): download file

* test(backend): remove code-generate gateway unit test

* feat(frontend): add layer

You can use your existing network to build a lenet now

* feat(backend): remove generate res

* fix(frontend): retry

* fix: group generate code fail

* fix: build error

* 🐳 chore: frontend image

* chore: add packaging strategy

* fix: expose 9000-9900 ports

* chore: ignore all conf file when docker build

* feat: deploy example

* feat: expose bundle.json

* feat(backend): get material

* feat(frontend): unexpose bundle.json

* test: skip ast

* feat(frontend): get material

* feat: README.md

* feat(bundle): bundle data

* feat: update css

* feat: ref schema

* feat(font): list data sturct support

* feat(backend): support list data-struct

* feat(bundle): upsample, SpectralNorm Unflatten

* test(back): ast unit test

* feat(back): user api

* feat: use secret at test

* feat: Add environment variables to dockerfile

* fix: ci error

* feat(back): auth guard

* feat(user): register

* feat(front): login & register

* feat(backend): auth at code generate

* feat: remove dashboard

* refactor: group generate

* styled: clean comment code

* feat: build sequential

* feat(backend): Sequential

* feat(backend): auto load bundle.json

* feat: update example bundle.json

* doc: update readme

* doc: update readme & docker-compose

* fix(backend): jwt token expire

* fix: data will never is empty

* fix: force get schema

* feat: Improve link logic

* fix(backend): stack overflow at sequencingNode

* feat: Fix the issue of untraceable child nodes

* feat: Nested groups are not allowed

* feat: completion bundle.json

* fix(backend): bcrypt error

* fix(backend): restore auth in gateway

* feat(backend): ws exception

* feat(frontend): error retry

* feat: salt len

* fix: ci error

* fix: when process.env.PWD_SALT is undefined

* fix(backend): salt error

* refactor(addons): clean toolbars

* refactor(canvas): remove Unused components

* doc: perfect document

* docs: Discussion on the Commutability of Function Combinations

* docs: complete the images in the document

* doc: refinish documents

* 更新 README.md

* doc: improve documentation and annotations

* docs: improve documentation

* refactor: repo sturct

* fix: dry run

* fix: build error

* fix: build err

* fix(example): docker compose up -d can run

* feat: all in one start

* feat(dashboard): backend

* fix(backend): project:counter loss

* feat(frontend): infinity scroll load project

* feat(frontend): navigator

* feat(frontend): save schema

* feat(backend): add data field when return project info

* feat(backend): should take graphData when save

* feat(frontend): save and import data

* feat: welcome will take nick name

* fix: return data at create

* fix(front): edge loss

* fix: build error

* fix: build error

* fix: build error

* feat(front): rename

* feat(front): update state

* fix(rename): import style

* feat(frontend): menu

* feat(frontend): menu

* fix: add loss entry

* fix: add loss dep

* fix: prettier error

* fix: collapse error

* fix: do not use jsx

* feat: menu take style

* fix(backend):when paramattr name is empty, use uuid

* feat(backend): expose example volume

* fix: add padding property

*  feat(example): add example

* docs: update README

* fix: mirror error

---------

Co-authored-by: yaoyun8 <142570291+yaoyun8@users.noreply.github.com>
Co-authored-by: lizhijie429 <632163606@qq.com>
Co-authored-by: wenmine <wwmmail@foxmail.com>
Co-authored-by: Lu17301156525 <128358973+Lu17301156525@users.noreply.github.com>
Co-authored-by: rhlin <r.h.lin@163.com>
Co-authored-by: chilingling <26962197+chilingling@users.noreply.github.com>
Co-authored-by: Gene <Pacify.98@gmail.com>
2024-05-09 15:27:21 +08:00
3501 changed files with 112760 additions and 298455 deletions

View File

@ -1,168 +0,0 @@
---
name: tinyengine-dsl-generator
description: Use when creating or modifying TinyEngine low-code applications - generating page, block, or app DSL (JSON schemas), converting designs/screenshots to DSL, or debugging generated TinyEngine JSON.
---
# TinyEngine DSL Generator
Generate conformant DSL (JSON) for the TinyEngine low-code platform: **pages**, **blocks**, and **apps**. This file is a router — load the reference files on demand for detail instead of reading everything up front.
## Quick Reference
| Task | Do |
| ------------------ | ----------------------------------------------------- |
| Generate page DSL | Describe components, layout, interactions → §Workflow |
| Generate block DSL | Describe reusable functionality + configurable props |
| Generate app DSL | Describe multi-page structure + shared componentsMap |
| From screenshot | Describe layout → map to components (§Design-to-DSL) |
| Lookup a component | `node scripts/query_components.mjs props <Name>` |
| Validate output | `bash scripts/validate_all.sh <file>` (required) |
## Workflow
1. **Understand the goal** — Page (components / state / methods / lifeCycles), Block (reusable, exposes a props `schema`), or App (pages + `componentsMap` + `meta`).
2. **Gather requirements** — name / route / title; component hierarchy; state; event handlers; data sources. Blocks additionally: exposed props, emitted events. Apps additionally: all pages, shared `componentsMap`.
3. **Load only the reference you need:**
| Need | File |
| ----------------------------------------------------------- | ---------------------------------------------------------- |
| Schema structure, TS interfaces, reserved names, prop types | [protocol.md](references/protocol.md) |
| Component props/events, or a component not listed here | [components.md](references/components.md) · `query_components.mjs` |
| List page / form page / layout / interaction templates | [patterns.md](references/patterns.md) |
⚠️ **Before generating any page with interactions**, read the event-binding section of [protocol.md](references/protocol.md). Event handlers are the #1 error source; the compact Critical Rules table below is a reminder, not a substitute for the full ❌/✅ example.
4. **Generate** — follow the Page skeleton + property types below. Full Page/Block/Component interfaces are in protocol.md.
5. **Validate** (required) — see §Validate.
6. **Run the checklist** before handing off — see §Pre-Generation Checklist.
### Page skeleton (anchor)
```json
{
"componentName": "Page",
"fileName": "PageName",
"meta": { "id": 1, "title": "...", "router": "...", "creator": "...", "isHome": false, "parentId": "0", "rootElement": "div", "group": "staticPages" },
"state": {},
"methods": {},
"lifeCycles": {},
"children": []
}
```
### Property value types
- **Literal**: `"text"`, `123`, `true`
- **JSExpression**: `{"type":"JSExpression","value":"this.state.count"}` — bindings, conditions, **event handlers**
- **JSFunction**: `{"type":"JSFunction","value":"function(){}"}`**only** inside `methods` / `lifeCycles`
- **i18n**: `{"type":"i18n","key":"app.title"}`
- **JSResource**: `{"type":"JSResource","value":"this.utils.format()"}`
### Referencing a block
```json
{ "componentName": "BlockFileName", "componentType": "block", "id": "block-001", "props": { "title": "value" } }
```
Inside the block: read `this.props.xxx`, emit via `this.emit('eventName', data)`.
## Critical Rules (common pitfalls)
These cause silent failures. Full ❌/✅ JSON examples live in [protocol.md](references/protocol.md); the checklist below enforces them.
| Rule | ❌ Wrong | ✅ Right |
| ------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| **Event bindings** | `"onClick":{"type":"JSFunction",...}`; or `JSExpression.value` = `"function…"` | `"onClick":{"type":"JSExpression","value":"this.handleX"}` — put the body in `methods` as `JSFunction` |
| **Method params** | `function(filter){...}` | `function(event, filter){...}`; binding `"params":["'all'"]` → call `handleX(event,'all')` |
| **Lifecycle name** | `"mounted":{...}` | `"onMounted":{"type":"JSFunction","value":"function onMounted(){...}"}` |
| **Two-way binding**| `modelValue` with **no** `model` | `"model":true` (v-model) or `"model":{"prop":"x"}` (v-model:x) |
| **Page editable** | `"occupier": {...}` | `"occupier": null` |
| **CSS class** | `props.class` | `props.className` |
### Event bindings — the full pattern (highest-frequency error)
The function body lives in `methods` (`JSFunction`); the event only **references** it (`JSExpression`). `event` is always the first arg; `params` append after.
```json
"methods": {
"handleDelete": {
"type": "JSFunction",
"value": "function(event, id) { this.state.list = this.state.list.filter(x => x.id !== id); }"
}
},
"children": [{
"componentName": "TinyButton",
"props": {
"text": "删除",
"onClick": { "type": "JSExpression", "value": "this.handleDelete", "params": ["123"] }
}
}]
```
The binding above calls `handleDelete(event, 123)`. ❌ Never put a `JSFunction` on an event, and never put a `function(){}` body inside a `JSExpression.value` — both silently break the handler.
**Memory aid:** `JSExpression` = reference (`this.fn`) · `JSFunction` = definition (`function(){}`). Events use references; methods use definitions.
## Validate (required)
```bash
bash .agents/skills/tinyengine-dsl-generator/scripts/validate_all.sh <output-file>
```
`validate_all.sh` chains three checks — do **not** rely on `validate_dsl.mjs` alone (it misses event-binding errors). Fix and re-run until all three pass; never hand off unvalidated output.
| Stage | Script | Catches |
| -------------- | ------------------------ | ------------------------------------------------------------------------------- |
| Structure | validate_dsl.mjs | Required fields, Page/Block `componentName`, meta, `class` vs `className`, app/page id types |
| Event bindings | check_event_bindings.mjs | `JSFunction` on an event, or a function body in `JSExpression.value` |
| CSS | check_css.mjs | Malformed `css` strings |
## Pre-Generation Checklist
- [ ] Event bindings use `JSExpression`; no `value` starts with `"function"`; function bodies live in `methods` / `lifeCycles`
- [ ] Event methods take `event` as the first parameter; `params` append after it
- [ ] Lifecycle names start with `on` (`onMounted`, …); `setup` is the only exception
- [ ] `modelValue` declares `model` (`true` for standard v-model)
- [ ] `occupier` is `null`
- [ ] All `id`s are unique; CSS classes use `className`, not `class`
- [ ] **App schema** `id` and `meta.appId` are integers (`918`, not `"918"`) — apps.js persists `meta.appId` as string internally, keep the DSL integer
- [ ] **Page** `app` reference is a string (`"918"`, not `918`) — pages.js queries with `appId.toString()`; a numeric `app` won't be found by `list()`. Page's own `id` is a NanoID string assigned by the server
## Component lookup
Don't load `bundle.json` (≈1 MB) by hand. Query it:
```bash
node scripts/query_components.mjs list # all components
node scripts/query_components.mjs props TinyButton # one component's props (fuzzy match)
node scripts/query_components.mjs cat 表单 # components in a category
node scripts/query_components.mjs search 表格 # full-text search
```
## File output
- **Apps**`mockServer/data/apps/<app-name>.json`
- **Pages**`mockServer/data/pages/<PageName>.json`
- **Blocks**`mockServer/data/blocks/<BlockName>.json`
Pages and blocks are saved with an **outer wrapper** around the DSL: page files wrap the Page DSL in `page_content` (plus `name`, `id`, `app`, `route`, `tenant`, `parentId`, `group`, `isPage`, `isHome`); block files wrap the Block DSL in `content` (plus `id`, `label`, `framework`, `path`, `public`, `is_published`). The validators auto-unwrap both, so you can validate either the wrapper or the inner DSL directly.
## Design-to-DSL
From a description or screenshot: identify layout regions → map visuals to components → extract interactions → define state + handlers → apply `className` / `style`.
## Troubleshooting
| Problem | Check |
| ----------------- | ---------------------------------------------------------------------- |
| Input not working | `modelValue` declares `model` (`true` for standard v-model) |
| Event not firing | `JSExpression` (not `JSFunction`); method exists in `methods` |
| Page not editable | `occupier` is `null` |
| Wrong params | first param is always `event`; `params` append after |
## Resources
- [protocol.md](references/protocol.md) — schema spec, TS interfaces, reserved names, property types, slots, full ❌/✅ examples
- [components.md](references/components.md) — component catalog (props/events); supplement with `query_components.mjs`
- [patterns.md](references/patterns.md) — list/form page, layout, interaction templates
- `scripts/``validate_all.sh` (run this), `validate_dsl.mjs`, `check_event_bindings.mjs`, `check_css.mjs`, `validate_page.mjs`, `query_components.mjs`

View File

@ -1,474 +0,0 @@
# TinyEngine Components Reference
本文档包含 TinyEngine 可用组件的快速参考。
## 组件来源
组件清单位于项目根目录的 `designer-demo/public/mock/bundle.json` 文件中。
## 组件分类
| 分类 | 组件数量 | 说明 |
| ------------ | -------- | ----------------- |
| general | 1+ | 通用基础组件 |
| html | 10 | HTML 原生元素 |
| 容器组件 | 2 | 布局容器 |
| 图表组件 | 12 | 数据可视化 |
| 组件 | 8 | 业务组件 |
| 评分组件 | 1 | 评分输入 |
| 进度条 | 1 | 进度显示 |
| 骨架屏 | 1 | 加载占位 |
| 滑块组件 | 1 | 滑块输入 |
| 步骤条 | 1 | 步骤导航 |
| element-plus | 7 | Element Plus 组件 |
| Other | 33+ | 其他组件 |
## 常用基础组件
### 按钮 (TinyButton)
```typescript
interface TinyButtonProps {
text: string // 按钮文字
type: 'primary' | 'success' | 'warning' | 'danger' | 'info'
size: 'medium' | 'small' | 'mini'
disabled: boolean
plain: boolean // 朴素按钮
round: boolean // 圆角
circle: boolean // 圆形
loading: boolean // 加载中
icon: string // 图标类名
onClick: JSFunction // 点击事件
}
```
### 输入框 (TinyInput)
```typescript
interface TinyInputProps {
modelValue: string | number;
type: 'text' | 'number' | 'password' | 'textarea';
placeholder: string;
disabled: boolean;
readonly: boolean;
clearable: boolean; // 可清空
size: 'medium' | 'small' | 'mini';
maxlength: number;
onChange: JSExpression; // ⚠️ 使用 JSExpression 引用方法
onFocus: JSExpression;
onBlur: JSExpression;
onKeyup: JSExpression; // 键盘事件 (如 Enter 键处理)
}
// ⚠️ 双向绑定示例:
"modelValue": {
"type": "JSExpression",
"value": "this.state.inputText",
"model": true // 标准双向绑定 (v-model)
}
// ⚠️ 事件绑定示例:
"onChange": {
"type": "JSExpression",
"value": "this.handleInputChange"
}
// methods 中定义:
"handleInputChange": {
"type": "JSFunction",
"value": "function(event) { this.state.inputText = event; }"
}
// Enter 键处理示例:
"onKeyup": {
"type": "JSExpression",
"value": "this.handleInputKeyup"
}
// methods 中定义:
"handleInputKeyup": {
"type": "JSFunction",
"value": "function(event) { if (event.keyCode === 13) { this.submitForm(event); } }"
}
```
### 表格 (TinyGrid)
```typescript
interface TinyGridProps {
data: Array<any> // 表格数据
columns: Array<{
// 列配置
field: string
title: string
width?: number
fixed?: 'left' | 'right'
align?: 'left' | 'center' | 'right'
editor?: {
component: string
type?: 'visible' | 'default'
}
}>
border: boolean
stripe: boolean // 斑马纹
height: string | number
autoResize: boolean
}
```
### 对话框 (TinyDialogBox)
```typescript
interface TinyDialogBoxProps {
visible: boolean // 是否显示
title: string
width: string
fullscreen: boolean
top: string
modal: boolean
lockScroll: boolean
beforeClose: JSFunction
onClose: JSFunction
}
```
### 选择器 (TinySelect)
```typescript
interface TinySelectProps {
modelValue: string | number | Array<any>
multiple: boolean
disabled: boolean
clearable: boolean
placeholder: string
options: Array<{
label: string
value: any
disabled?: boolean
}>
remote: boolean // 远程搜索
remoteMethod: JSFunction
onChange: JSFunction
}
```
### 标签页 (TinyTabs)
```typescript
interface TinyTabsProps {
activeName: string
type: '' | 'card' | 'border-card'
tabPosition: 'top' | 'right' | 'bottom' | 'left'
stretch: boolean
onTabClick: JSFunction
}
// 子项 TinyTabItem
interface TinyTabItemProps {
title: string
name: string
disabled: boolean
}
```
### 表单 (TinyForm)
```typescript
interface TinyFormProps {
modelValue: Record<string, any>;
rules: Record<string, any>; // 校验规则
labelWidth: string;
labelPosition: 'left' | 'right' | 'top';
inline: boolean;
disabled: boolean;
validate: JSExpression;
resetFields: JSExpression;
}
// ⚠️ 表单双向绑定示例:
"modelValue": {
"type": "JSExpression",
"value": "this.state.formData",
"model": true // 标准双向绑定 (v-model)
}
// ⚠️ 表单内输入框双向绑定:
"modelValue": {
"type": "JSExpression",
"value": "this.state.formData.name",
"model": true // 标准双向绑定用 true{prop} 仅用于具名 v-model (v-model:xxx)
}
// 表单验证示例:
"methods": {
"handleSubmit": {
"type": "JSFunction",
"value": "async function(event) { const valid = await this.$refs.formRef.validate(); if (valid) { /* 提交表单 */ } }"
}
}
// 表单项 TinyFormItem
interface TinyFormItemProps {
label: string;
prop: string;
required: boolean;
rules: Array<any>;
}
```
### 布局组件
#### 容器 (div)
```typescript
interface DivProps {
className: string
style: string
}
```
#### Span (span)
```typescript
interface SpanProps {
className: string
style: string
}
```
#### 图标 (Icon)
```typescript
interface IconProps {
name: string // 图标名称,如 "IconChevronLeft"
style: string
className: string
}
```
#### 文本 (Text)
```typescript
interface TextProps {
text: string | II18n // 支持i18n
style: string
className: string
}
```
### 数据展示
#### 树形控件 (TinyTree)
```typescript
interface TinyTreeProps {
data: Array<{
label: string
children?: Array<any>
id: string | number
}>
showCheckbox: boolean
checkOnClickNode: boolean
defaultExpandAll: boolean
filterNodeMethod: JSFunction
onCheckChange: JSFunction
onNodeClick: JSFunction
}
```
#### 分页 (TinyPager)
```typescript
interface TinyPagerProps {
currentPage: number
pageSizes: Array<number>
pageSize: number
total: number
layout: string // 如 "total, sizes, prev, pager, next, jumper"
onCurrentChange: JSFunction
onSizeChange: JSFunction
}
```
#### 进度条 (TinyProgress)
```typescript
interface TinyProgressProps {
percentage: number // 0-100
type: 'line' | 'circle' | 'dashboard'
status: 'success' | 'exception' | 'warning'
strokeWidth: number
color: string | string[]
}
```
### 开关和选择
#### 开关 (TinySwitch)
```typescript
interface TinySwitchProps {
modelValue: boolean
disabled: boolean
width: number
activeText: string
inactiveText: string
onChange: JSFunction
}
```
#### 单选框 (TinyRadio)
```typescript
interface TinyRadioProps {
modelValue: string | number | boolean
label: string | number
disabled: boolean
border: boolean
onChange: JSFunction
}
// 单选组 TinyRadioGroup
interface TinyRadioGroupProps {
modelValue: any
size: 'medium' | 'small' | 'mini'
fill: string
textColor: string
onChange: JSFunction
}
```
#### 复选框 (TinyCheckbox)
```typescript
interface TinyCheckboxProps {
modelValue: boolean | string | number
label: string
trueLabel: string
falseLabel: string
disabled: boolean
border: boolean
onChange: JSFunction
}
// 复选组 TinyCheckboxGroup
interface TinyCheckboxGroupProps {
modelValue: Array<any>
size: string
min: number
max: number
onChange: JSFunction
}
```
#### 日期选择 (TinyDatePicker)
```typescript
interface TinyDatePickerProps {
modelValue: string | Date
type: 'year' | 'month' | 'date' | 'dates' | 'week' | 'datetime' | 'datetimerange' | 'daterange'
placeholder: string
startPlaceholder: string
endPlaceholder: string
format: string
disabled: boolean
clearable: boolean
onChange: JSFunction
}
```
### 消息提示
#### 警告 (TinyAlert)
```typescript
interface TinyAlertProps {
title: string
type: 'success' | 'warning' | 'info' | 'error'
description: string
closable: boolean
center: boolean
closeText: string
showIcon: boolean
}
```
#### 消息 (TinyModal)
```typescript
// 使用方法
{
"type": "JSResource",
"value": "this.$modal.message({ message: '操作成功', status: 'success' })"
}
```
#### 确认框 (TinyConfirm)
```typescript
// 使用方法
{
"type": "JSResource",
"value": "this.$modal.confirm({ message: '确定删除?' }).then(() => {})"
}
```
## 图表组件
### 折线图 (TinyLineChart)
```typescript
interface TinyLineChartProps {
data: Array<any>
settings: {
dimensions?: string[]
metrics?: string[]
xAxisType?: 'category' | 'value' | 'time'
yAxisType?: 'category' | 'value' | 'time'
yAxisName?: string[]
area?: boolean
}
}
```
### 柱状图 (TinyBarChart)
```typescript
interface TinyBarChartProps {
data: Array<any>
settings: {
dimensions?: string[]
metrics?: string[]
axisSite?: { top?: string[] }
label?: { show?: boolean }
}
}
```
### 饼图 (TinyPieChart)
```typescript
interface TinyPieChartProps {
data: Array<any>
settings: {
dimension?: string
metrics?: string[]
radius?: number | number[]
}
}
```
## 组件查询
不要手动 grep bundle.json≈1MB。用查询脚本
```bash
node scripts/query_components.mjs props TinyButton # 某组件的属性表(模糊匹配)
node scripts/query_components.mjs list # 全部组件
node scripts/query_components.mjs cat 表单 # 某分类下的组件
node scripts/query_components.mjs search 表格 # 全字段搜索
```
## 常见问题与规则
通用规则(事件绑定、双向绑定 `model`、`occupier`、`className`、生命周期、参数传递)的精简表见 [SKILL.md](../SKILL.md)「Critical Rules / Troubleshooting」完整 ❌/✅ 示例与 TS 接口见 [protocol.md](protocol.md);条件渲染 / 循环渲染等模式见 [patterns.md](patterns.md)。本文件专注于各组件的 props/events 参考。

File diff suppressed because it is too large Load Diff

View File

@ -1,595 +0,0 @@
# TinyEngine DSL Protocol Reference
本文档包含 TinyEngine 低代码平台 DSL 协议的完整参考,用于生成符合协议规范的 JSON 数据。
## 目录
1. [应用协议](#应用协议)
2. [页面结构](#页面结构)
3. [组件结构](#组件结构)
4. [区块结构](#区块结构)
5. [保留字](#保留字)
6. [插槽语法](#插槽语法)
7. [数据源](#数据源)
8. [国际化](#国际化)
---
## 应用协议
### 应用结构
```typescript
interface IAppSchema {
version: string // 协议版本号,如 "1.0.0"
componentsMap: IComponentMap[] // 组件映射关系
componentsTree: IPageSchema[] // 应用包含的页面列表
bridge: IBridge[] // 桥接源(工具函数、依赖)
meta: IAppMeta // 应用基础信息
dataSource?: IDataSource // 应用级数据源
i18n?: II18n // 应用级国际化
utils?: any[] // 工具类
constants?: Record<string, any> // 常量
css?: string // 全局CSS
config?: IAppConfig // 应用配置
}
interface IComponentMap {
componentName: string // 渲染时使用的组件名
package: string // npm包名
version: string // 版本号
destructuring: boolean // 是否解构
exportName: string // 导出名
subName?: string // 子导出名
}
interface IAppMeta {
appId: string | number // App Schema 中建议使用整数 (e.g., 918);服务端持久化时会转成字符串
name: string
description: string
creator: string
git_group?: string
project_name?: string
gmt_create: string
gmt_modified: string
}
/**
* App ID 格式建议:
* - App Schema 文件中的 `id` 字段: 整数类型 (e.g., 918)
* - App Schema 文件中的 `meta.appId` 字段: 整数类型 (e.g., 918)
* - App Metadata 文件中的 `id` 字段: 整数类型 (e.g., 918)
* - Page 文件中的 `app` 字段: 字符串类型 (e.g., "918")
*
* 注意: pages.js 使用 appId.toString() 查询页面,直接落盘的 Page 文件必须用字符串 app 引用。
*/
interface IAppConfig {
sdkVersion: string
historyMode: 'hash' | 'browser'
targetRootID: string
}
interface IBridge {
name: string
type: 'npm' | 'function'
content?: {
package?: string
version?: string
exportName?: string
subName?: string
destructuring?: boolean
main?: string
}
}
```
---
## 页面结构
### 页面 Schema
```typescript
interface IPageSchema {
componentName: 'Page' // 固定值
fileName: string // 页面文件名
meta: IPageMeta // 页面元信息
props?: IProps // 页面属性
state?: Record<string, any> // 页面状态
methods?: Record<string, IJSFunction> // 页面方法
lifeCycles?: Record<string, IJSFunction> // 生命周期
children?: IComponentSchema[] | string // 子组件
css?: string // 页面CSS (换行必须转义为 \n)
dataSource?: IDataSource // 页面数据源
utils?: any[] // 页面工具函数
bridge?: IBridge[] // 页面桥接源
occupier?: null | IOccupier // ⚠️ 必须为 null 才能编辑页面
}
interface IPageMeta {
id: number
title: string
description?: string
router: string // 不能以 / 开头,不支持路由参数 xxx/:id
creator: string
isHome: boolean
parentId: string // 顶层时为 "0"
rootElement: string // 如 "div"
group: string // 如 "staticPages"
gmt_create: string
gmt_modified: string
}
interface IOccupier {
id: number
username: string
email: string
is_admin: boolean
}
```
### 页面示例
```json
{
"componentName": "Page",
"fileName": "HomePage",
"meta": {
"id": 1,
"title": "首页",
"router": "home",
"creator": "admin",
"isHome": true,
"parentId": "0",
"rootElement": "div",
"group": "staticPages",
"description": "应用首页",
"gmt_create": "2024-01-01 00:00:00",
"gmt_modified": "2024-01-01 00:00:00"
},
"props": {},
"state": {
"count": 0,
"message": "Hello"
},
"methods": {
"handleClick": {
"type": "JSFunction",
"value": "function(event) { this.state.count++ }"
}
},
"lifeCycles": {
"onMounted": {
"type": "JSFunction",
"value": "function onMounted() { console.log('Page mounted'); }"
}
},
"css": ".container { padding: 20px; }",
"occupier": null,
"children": []
}
```
### 生命周期钩子
可用的生命周期名称(必须以 `on` 开头):
| 生命周期 | 说明 | Vue 等价 |
| ----------------- | ------------------- | ----------------- |
| `setup` | 组合式 API 设置入口 | setup() |
| `onBeforeMount` | 挂载前 | onBeforeMount() |
| `onMounted` | 挂载后 | onMounted() |
| `onBeforeUpdate` | 更新前 | onBeforeUpdate() |
| `onUpdated` | 更新后 | onUpdated() |
| `onBeforeUnmount` | 卸载前 | onBeforeUnmount() |
| `onUnmounted` | 卸载后 | onUnmounted() |
**setup 生命周期特殊参数**:
```json
{
"lifeCycles": {
"setup": {
"type": "JSFunction",
"value": "function setup({ props, state, watch, onMounted, onUpdated }) {\n // 使用这些参数进行响应式编程\n watch(() => props.data, (newVal) => { console.log('Data changed:', newVal); });\n}"
}
}
}
```
---
## 组件结构
### 组件 Schema
```typescript
interface IComponentSchema {
componentName: string // 组件名或区块名
componentType?: 'block' // 为区块时设置此值
id: string // 唯一ID
props?: IProps // 组件属性
children?: IComponentSchema[] | string // 子组件
condition?: ICondition // 条件渲染
}
interface IProps {
[key: string]: IPropValue | any
}
type IPropValue = string | number | boolean | Array<any> | Object | IJSExpression | II18n | IJSFunction | IJSResource
interface IJSExpression {
type: 'JSExpression'
value: string // 如 "this.state.count"
model?: boolean | { prop: string } // 双向绑定: true 表示 v-model{prop:"xxx"} 表示 v-model:xxx
params?: string[] // 事件附加参数 (追加在 event 之后)
}
// ⚠️ 事件绑定规则 (CRITICAL - 常见错误区域):
// 1. 事件绑定必须使用 JSExpression不能用 JSFunction
// 2. 引用 methods 中定义的方法
// 3. 第一个参数自动是 eventparams 中的参数追加在后面
// 4. ❌ 禁止JSExpression 的 value 中包含 function 定义
// 示例:
// ✅ 正确 - 绑定: "onClick": { "type": "JSExpression", "value": "this.handleClick", "params": ["'id'"] }
// ❌ 错误 - 绑定: "onClick": { "type": "JSExpression", "value": "function(event) { ... }" }
// 调用: handleClick(event, 'id')
// 方法定义: "handleClick": { "type": "JSFunction", "value": "function(event, id) { ... }" }
//
// 记忆口诀:
// - JSExpression = 引用 (this.methodName)
// - JSFunction = 定义 (function() {...})
// - 事件绑定用引用 (JSExpression),方法定义用函数 (JSFunction)
// ⚠️ 双向绑定规则:
// 1. 标准双向绑定使用 model: true
// "modelValue": { "type": "JSExpression", "value": "this.state.text", "model": true }
// 等价于 Vue: v-model="state.text"
// 2. 具名双向绑定使用 model: { "prop": "xxx" }
// "visible": { "type": "JSExpression", "value": "this.state.visible", "model": { "prop": "visible" } }
// 等价于 Vue: v-model:visible="state.visible"
interface II18n {
type: 'i18n'
key: string // 国际化key
}
interface IJSFunction {
type: 'JSFunction'
value: string // 函数字符串
}
interface IJSResource {
type: 'JSResource'
value: string // 如 "this.utils.formatDate()"
}
interface ICondition {
type: 'JSExpression'
value: string // 条件表达式
}
```
### 属性类型说明
| 类型 | 说明 | 示例 |
| -------------- | ---------- | -------------------------------------------------------- |
| 字面值 | 直接值 | `"text"`, `123`, `true` |
| `JSExpression` | 表达式绑定 | `{"type": "JSExpression", "value": "this.state.count"}` |
| `i18n` | 国际化 | `{"type": "i18n", "key": "app.title"}` |
| `JSFunction` | 函数 | `{"type": "JSFunction", "value": "function() {}"}` |
| `JSResource` | 资源引用 | `{"type": "JSResource", "value": "this.utils.format()"}` |
### ⚠️ 常见错误:事件绑定中的类型混淆
**错误示例** (在事件绑定中使用 `JSExpression``value` 中包含函数定义):
```json
// ❌ 错误
{
"componentName": "TinyButton",
"props": {
"onClick": {
"type": "JSExpression",
"value": "function(event) { this.doSomething(); }"
}
}
}
```
**正确做法** (将函数定义放在 `methods` 中,事件绑定引用方法):
```json
// ✅ 正确
{
"methods": {
"handleClick": {
"type": "JSFunction",
"value": "function(event) { this.doSomething(); }"
}
},
"children": [
{
"componentName": "TinyButton",
"props": {
"onClick": {
"type": "JSExpression",
"value": "this.handleClick"
}
}
}
]
}
```
**关键规则**:
- `JSExpression.value` = 方法引用 (如 `this.methodName`)
- `JSFunction.value` = 函数定义 (如 `function() {...}`)
- 事件绑定用 `JSExpression`,函数定义用 `JSFunction`
---
## 区块结构
### 区块 Schema
```typescript
interface IBlockSchema {
componentName: 'Block' // 固定值
fileName: string // 区块文件名
label: string // 区块HTML标签
css?: string // 区块CSS
props?: IProps // 区块属性(可配置)
state?: Record<string, any> // 区块状态
methods?: Record<string, IJSFunction> // 区块方法
lifeCycles?: Record<string, IJSFunction> // 生命周期
schema: IBlockSchemaConfig // 区块对外暴露的配置schema
children?: IComponentSchema[] // 区块内容
dataSource?: IDataSource // 区块数据源
}
interface IBlockSchemaConfig {
properties: IPropertyConfig[] // 可配置属性
events?: Record<string, IEventConfig> // 可触发事件
slots?: Record<string, any> // 插槽定义
}
interface IPropertyConfig {
label: { zh_CN: string }
description?: { zh_CN: string }
content: IPropertyItem[]
}
interface IPropertyItem {
property: string // 属性名
type: string | string[] // 属性类型
defaultValue: any // 默认值
label: { text: { zh_CN: string } }
widget: {
// 配置组件
component: string
props?: any
}
required?: boolean
cols?: number
}
```
### 区块使用示例
在页面中引用区块:
```json
{
"componentName": "MyBlock", // 区块的fileName
"componentType": "block",
"id": "block-001",
"props": {
"title": "标题", // 传递给区块的props
"data": {
"type": "JSExpression",
"value": "this.state.list"
}
}
}
```
---
## 保留字
以下`componentName`为保留关键字,不允许使用同名物料:
| ComponentName | 说明 | 用途 |
| ------------- | -------------------- | ------------------------------------- |
| `Page` | 页面容器 | 配合`fileName`确定页面名称 |
| `Block` | 区块容器 | 配合`fileName`确定区块名称 |
| `Component` | 业务组件容器(预留) | - |
| `Template` | 虚拟容器,不渲染 | 用于具名插槽children 为[]时出码跳过 |
| `Slot` | 插槽定义 | 定义具名插槽 |
| `Collection` | 数据源容器,不渲染 | 提供数据源,出码跳过 |
| `Text` | 文本节点 | 使用 span 渲染text 属性包含内容 |
---
## 插槽语法
### 定义插槽
```json
{
"componentName": "slot",
"props": {
"name": "formSlot"
},
"children": [
{
"componentName": "tiny-input",
"props": {}
}
]
}
```
生成代码:`<slot><tiny-input></tiny-input></slot>`
### 使用作用域插槽
```json
{
"componentName": "template",
"props": {
"slot": {
"name": "footer",
"params": ["row"]
}
},
"children": [...]
}
```
生成代码:`<template #footer="{ row }">...</template>`
### 表格插槽示例
```json
{
"slots": {
"header": {
"type": "JSSlot",
"params": ["column"],
"value": [
{
"componentName": "div",
"children": [...]
}
]
}
}
}
```
---
## 数据源
### 数据源结构
```typescript
interface IDataSource {
dataHandler?: string
list: IDataSourceItem[]
}
interface IDataSourceItem {
id: string | number
name: string
desc?: string
app: string
type: 'fetch' | 'value'
data?: {
columns?: Array<any>
data?: Array<any>
dataHandler?: IJSFunction
errorHandler?: IJSFunction
option?: {
method: string
url: string
}
shouldFetch?: IJSFunction
willFetch?: IJSFunction
}
value?: {
data?: Array<any>
columns?: Array<any>
}
}
```
### Collection 容器使用数据源
```json
{
"componentName": "collection",
"props": {
"dataSource": "tableData"
},
"children": [
{
"componentName": "tiny-grid",
"props": {
"data": {
"type": "JSExpression",
"value": "this.tableData"
},
"columns": [...]
}
}
]
}
```
---
## 国际化
### i18n 结构
```typescript
interface II18n {
[locale: string]: {
[key: string]: string // key-value对支持模板如 "Hello ${name}"
}
}
```
### i18n 示例
```json
{
"i18n": {
"zh-CN": {
"app-title": "我的应用",
"welcome": "你好 ${name}"
},
"en-US": {
"app-title": "My App",
"welcome": "Hello ${name}"
}
}
}
```
### 使用 i18n
在 props 中:
```json
{
"props": {
"text": {
"type": "i18n",
"key": "app-title"
}
}
}
```
带参数:
```json
{
"props": {
"text": {
"type": "i18n",
"key": "welcome",
"params": {
"name": "World"
}
}
}
}
```

View File

@ -1,179 +0,0 @@
#!/usr/bin/env node
/**
* TinyEngine CSS Syntax Checker
*
* 检查DSL中的CSS字段是否有语法错误基础模式括号匹配基本语法无需额外依赖
*
* 零依赖仅用 Node 标准库
* tinycss2 / postcss 模式依赖外部环境已精简basic 是默认且为编排脚本使用的模式
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
/** 普通对象判定(非 null、非数组 */
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* 解包外层包装结构返回内层 DSL页面在 page_content 区块在 content
* 仅当内层确实是含 componentName 的节点时才解包否则原样返回
*/
function extractInnerDsl(dslData) {
if (isPlainObject(dslData)) {
for (const key of ['page_content', 'content']) {
const inner = dslData[key];
if (isPlainObject(inner) && 'componentName' in inner) {
return inner;
}
}
}
return dslData;
}
/** CSS 语法检查器(基础模式,无需额外依赖) */
export class BasicCssChecker {
/** @param {*} dslData */
constructor(dslData) {
this.dsl = dslData;
this.errors = [];
this.warnings = [];
}
/** 检查 CSS 语法 */
check() {
// 从外层包装(page_content/content)解包到内层 DSL 后再读取 css
const inner = extractInnerDsl(this.dsl);
const cssString = inner.css ?? '';
if (!cssString) {
this.warnings.push('No CSS field found');
return true;
}
return this._checkCss(cssString);
}
/** 基础检查:括号匹配、基本语法 */
_checkCss(css) {
// 检查括号匹配
const stack = [];
for (let i = 0; i < css.length; i++) {
const char = css[i];
if (char === '{') {
stack.push([char, i]);
} else if (char === '}') {
if (stack.length === 0 || stack[stack.length - 1][0] !== '{') {
this.errors.push(`Unmatched '}' at position ${i}`);
return false;
}
stack.pop();
} else if (char === '(') {
stack.push([char, i]);
} else if (char === ')') {
if (stack.length === 0 || stack[stack.length - 1][0] !== '(') {
this.errors.push(`Unmatched ')' at position ${i}`);
return false;
}
stack.pop();
}
}
if (stack.length) {
for (const [char, pos] of stack) {
this.errors.push(`Unclosed '${char}' at position ${pos}`);
}
return false;
}
// 移除注释进行检查
const cssNoComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
// 检查是否有 CSS 规则
if (!cssNoComments.includes('{')) {
this.warnings.push('CSS may not contain any rules');
}
// 检查分号使用
const rules = [...cssNoComments.matchAll(/\{([^}]*)\}/g)].map((m) => m[1]);
for (const rule of rules) {
const properties = rule.split(';');
// 最后一个可能为空
for (const propRaw of properties.slice(0, -1)) {
const prop = propRaw.trim();
if (prop && !prop.includes(':')) {
this.warnings.push(`Property without colon: ${prop.slice(0, 50)}`);
}
}
}
return this.errors.length === 0;
}
/** 生成报告 */
report() {
const lines = [];
if (this.errors.length) {
lines.push('❌ CSS Errors:');
for (const error of this.errors) lines.push(` - ${error}`);
}
if (this.warnings.length) {
lines.push('⚠️ CSS Warnings:');
for (const warning of this.warnings) lines.push(` - ${warning}`);
}
if (this.errors.length === 0 && this.warnings.length === 0) {
lines.push('✅ CSS check passed!');
}
return lines.join('\n');
}
}
// 可用模式表(仅保留 basic
const CHECKERS = { basic: BasicCssChecker };
function main() {
if (process.argv.length < 3) {
console.log('Usage: check_css.mjs <dsl-file> [mode]');
console.log(' mode: basic (default)');
process.exit(1);
}
const filePath = process.argv[2];
const mode = process.argv[3] || 'basic';
// 读取 DSL 文件
let dslData;
try {
const text = fs.readFileSync(filePath, 'utf8');
dslData = JSON.parse(text);
} catch (e) {
if (e instanceof SyntaxError) {
console.log(`❌ Invalid JSON: ${e.message}`);
} else if (e.code === 'ENOENT') {
console.log(`❌ File not found: ${filePath}`);
} else {
throw e;
}
process.exit(1);
}
// 选择检查器
const CheckerClass = CHECKERS[mode];
if (!CheckerClass) {
console.log(`❌ Unknown mode: ${mode}`);
console.log(`Available modes: ${Object.keys(CHECKERS).join(', ')}`);
process.exit(1);
}
const checker = new CheckerClass(dslData);
const isValid = checker.check();
console.log(checker.report());
process.exit(isValid ? 0 : 1);
}
const __filename = fileURLToPath(import.meta.url);
if (path.resolve(process.argv[1] || '') === __filename) {
main();
}

View File

@ -1,185 +0,0 @@
#!/usr/bin/env node
/**
* TinyEngine Event Binding Checker
*
* 检查DSL文件中的事件绑定是否正确使用JSExpression引用方法
* 而不是在value中直接写函数定义
*
* 零依赖仅用 Node 标准库
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
/** 普通对象判定(非 null、非数组 */
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* 解包外层包装结构返回内层 DSL
* 落盘的页面/区块文件是"外层包装 + 内层 DSL"
* - 页面 DSL page_content
* - 区块 DSL content
* 仅当内层确实是含 componentName 的节点时才解包避免误吞同名普通字段否则原样返回
*/
function extractInnerDsl(dslData) {
if (isPlainObject(dslData)) {
for (const key of ['page_content', 'content']) {
const inner = dslData[key];
if (isPlainObject(inner) && 'componentName' in inner) {
return inner;
}
}
}
return dslData;
}
const EVENT_KEYS = [
'onClick', 'onChange', 'onKeyup', 'onKeyDown', 'onKeyPress',
'onFocus', 'onBlur', 'onSubmit', 'onInput', 'onTabClick',
'onCurrentChange', 'onSizeChange', 'onCheckChange',
'onNodeClick', 'onRowClick', 'onCellClick',
];
export class EventBindingChecker {
/** @param {*} dslData */
constructor(dslData) {
this.dsl = dslData;
this.errors = [];
this.warnings = [];
}
/** 检查所有事件绑定 */
check() {
// 从外层包装(page_content/content)解包到内层 DSL 后再检查
const inner = extractInnerDsl(this.dsl);
this._checkNode(inner);
return this.errors.length === 0;
}
/** 递归检查节点 */
_checkNode(node) {
if (isPlainObject(node)) {
// 检查当前节点的事件绑定
this._checkEventBindings(node);
// 递归检查子节点(仅当 children 是数组;字符串子节点无需处理)
if (Array.isArray(node.children)) {
for (const child of node.children) {
this._checkNode(child);
}
}
} else if (Array.isArray(node)) {
for (const item of node) {
this._checkNode(item);
}
}
}
/** 检查单个节点的事件绑定(含 props 内的事件绑定) */
_checkEventBindings(node) {
const component = Object.prototype.hasOwnProperty.call(node, 'componentName') ? node.componentName : 'unknown';
// 两处都需要校验,避免漏检 props 内的事件。
this._checkEventHolder(node, component);
if (isPlainObject(node.props)) {
this._checkEventHolder(node.props, component);
}
}
/** 检查某个属性容器(节点本身或其 props内的事件绑定 */
_checkEventHolder(holder, component) {
// 检查所有可能的事件属性
for (const key of EVENT_KEYS) {
if (key in holder) {
const value = holder[key];
if (isPlainObject(value)) {
this._checkEventValue(component, key, value);
}
}
}
// 也检查以 'on' 开头的属性
for (const [key, value] of Object.entries(holder)) {
if (key.startsWith('on') && !EVENT_KEYS.includes(key)) {
if (isPlainObject(value)) {
this._checkEventValue(component, key, value);
}
}
}
}
/** 检查事件值 */
_checkEventValue(component, eventKey, value) {
const valueType = value.type;
const valueContent = Object.prototype.hasOwnProperty.call(value, 'value') ? value.value : '';
// 错误1: 使用 JSFunction 类型进行事件绑定
if (valueType === 'JSFunction') {
this.errors.push(
`${component}.${eventKey}: 使用了JSFunction类型应该使用JSExpression引用methods中的方法`
);
}
// 错误2: JSExpression 的 value 中包含函数定义
if (valueType === 'JSExpression' && typeof valueContent === 'string' && valueContent.startsWith('function')) {
this.errors.push(
`${component}.${eventKey}: JSExpression的value中包含函数定义 '${valueContent.slice(0, 30)}...'` +
`应该引用方法如 'this.methodName'`
);
}
}
/** 生成报告 */
report() {
const lines = [];
if (this.errors.length) {
lines.push('❌ 发现事件绑定错误:');
for (const error of this.errors) lines.push(` - ${error}`);
}
if (this.warnings.length) {
lines.push('⚠️ 警告:');
for (const warning of this.warnings) lines.push(` - ${warning}`);
}
if (this.errors.length === 0 && this.warnings.length === 0) {
lines.push('✅ 所有事件绑定检查通过!');
}
return lines.join('\n');
}
}
function main() {
if (process.argv.length < 3) {
console.log('Usage: check_event_bindings.mjs <dsl-file>');
process.exit(1);
}
const filePath = process.argv[2];
let dslData;
try {
const text = fs.readFileSync(filePath, 'utf8');
dslData = JSON.parse(text);
} catch (e) {
if (e instanceof SyntaxError) {
console.log(`❌ Invalid JSON: ${e.message}`);
} else if (e.code === 'ENOENT') {
console.log(`❌ File not found: ${filePath}`);
} else {
throw e;
}
process.exit(1);
}
const checker = new EventBindingChecker(dslData);
const isValid = checker.check();
console.log(checker.report());
process.exit(isValid ? 0 : 1);
}
const __filename = fileURLToPath(import.meta.url);
if (path.resolve(process.argv[1] || '') === __filename) {
main();
}

View File

@ -1,215 +0,0 @@
#!/usr/bin/env node
/**
* TinyEngine Component Catalog Query
*
* designer-demo/public/mock/bundle.json 查询组件元数据
* 避免把 ~1MB 全量清单加载进上下文零依赖仅用 Node 标准库
*
* 用法:
* node query_components.mjs list 列出全部组件 / 中文名 / 分类 / 描述
* node query_components.mjs categories 列出所有分类及组件数
* node query_components.mjs cat <分类关键字> 列出某分类下的组件
* node query_components.mjs props <组件名或中文> 查某组件的属性表支持模糊匹配
* node query_components.mjs search <关键字> 按名 / 中文名 / 描述 / 分类搜索
*
* 可用 BUNDLE_JSON=<path> 环境变量覆盖 bundle.json 位置
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const SCRIPT_DIR = path.dirname(__filename);
// scripts/ → tinyengine-dsl-generator → skills → .agents → 仓库根
const REPO_ROOT = path.resolve(SCRIPT_DIR, '..', '..', '..', '..');
const DEFAULT_BUNDLE = path.join(REPO_ROOT, 'designer-demo', 'public', 'mock', 'bundle.json');
function loadComponents() {
const file = process.env.BUNDLE_JSON || DEFAULT_BUNDLE;
let json;
try {
json = JSON.parse(fs.readFileSync(file, 'utf8'));
} catch (e) {
if (e.code === 'ENOENT') {
console.error(`❌ 找不到 bundle.json: ${file}(可用 BUNDLE_JSON=<path> 覆盖)`);
} else if (e instanceof SyntaxError) {
console.error(`❌ bundle.json 解析失败: ${e.message}`);
} else {
throw e;
}
process.exit(1);
}
return json?.data?.materials?.components ?? [];
}
/** 取一个节点的中文标签,兼容 label.text.zh_CN / label.zh_CN / name.zh_CN */
function zhLabel(node) {
if (!node) return '';
return node.text?.zh_CN || node.label?.text?.zh_CN || node.label?.zh_CN || node.name?.zh_CN || '';
}
/** 组件名(强制字符串,少数条目 component 字段非字符串) */
function cname(c) {
return String(c?.component ?? '');
}
function truncate(s, n = 46) {
s = (s || '').replace(/\s+/g, ' ').trim();
return s.length > n ? s.slice(0, n - 1) + '…' : s;
}
function pad(s, n) {
s = String(s ?? '');
return s.length >= n ? s : s + ' '.repeat(n - s.length);
}
function listAll(comps) {
console.log(`${pad('componentName', 22)} ${pad('中文名', 12)} ${pad('category', 12)} description`);
console.log('-'.repeat(86));
for (const c of comps) {
console.log(`${pad(c.component, 22)} ${pad(zhLabel(c), 12)} ${pad(c.category || '', 12)} ${truncate(c.description)}`);
}
console.log(`\n${comps.length} 个组件`);
}
function categories(comps) {
const m = new Map();
for (const c of comps) {
const k = c.category || '(无分类)';
m.set(k, (m.get(k) || 0) + 1);
}
for (const [k, n] of [...m].sort((a, b) => b[1] - a[1])) {
console.log(`${pad(k, 16)} ${n}`);
}
}
function byCategory(comps, cat) {
const hits = comps.filter((c) => (c.category || '').toLowerCase().includes(cat.toLowerCase()));
if (hits.length === 0) {
console.log(`没有匹配分类 "${cat}" 的组件。运行 "categories" 查看全部分类。`);
return;
}
console.log(`分类匹配 "${cat}"${hits.length} 个):`);
for (const c of hits) console.log(` ${pad(c.component, 22)} ${pad(zhLabel(c), 12)} ${truncate(c.description)}`);
}
/** 精确 → 模糊componentName / 中文名)匹配,返回单个 / 多个 / 未命中 */
function findComp(comps, name) {
const exact = comps.find((c) => cname(c) === name);
if (exact) return { comp: exact };
const lower = name.toLowerCase();
const byName = comps.filter((c) => cname(c).toLowerCase().includes(lower));
if (byName.length === 1) return { comp: byName[0] };
const byZh = comps.filter((c) => zhLabel(c).includes(name));
if (byZh.length === 1) return { comp: byZh[0] };
const pooled = new Map();
for (const c of [...byName, ...byZh]) pooled.set(cname(c), c);
if (pooled.size > 0) return { multiple: [...pooled.values()] };
return {};
}
function showProps(comp) {
console.log(`${comp.component}${zhLabel(comp)}(分类: ${comp.category || '?'}`);
if (comp.description) console.log(comp.description);
if (comp.npm?.package) {
const imp = comp.npm.destructuring ? `{ ${comp.npm.exportName} }` : comp.npm.exportName;
console.log(`npm: ${comp.npm.package} ${imp}`);
}
console.log('');
const groups = comp.schema?.properties || [];
if (groups.length === 0) {
console.log('(该组件无 props schema');
return;
}
console.log(`${pad('property', 18)}${pad('widget', 22)}${pad('required', 9)}label / description`);
console.log('-'.repeat(86));
let count = 0;
for (const g of groups) {
for (const item of g.content || []) {
const prop = item.property || '?';
const widget = item.widget?.component || '';
const req = item.required ? 'required' : '';
const desc = truncate(item.description?.zh_CN || zhLabel(item), 40);
console.log(`${pad(prop, 18)}${pad(widget, 22)}${pad(req, 9)}${desc}`);
count++;
}
}
console.log(`\n${count} 个属性`);
}
function search(comps, kw) {
const lower = kw.toLowerCase();
const hits = comps.filter(
(c) =>
cname(c).toLowerCase().includes(lower) ||
zhLabel(c).includes(kw) ||
(c.description || '').toLowerCase().includes(lower) ||
(c.category || '').toLowerCase().includes(lower)
);
if (hits.length === 0) {
console.log(`没有匹配 "${kw}" 的组件。`);
return;
}
console.log(`匹配 "${kw}"${hits.length} 个):`);
for (const c of hits) console.log(` ${pad(c.component, 22)} ${pad(zhLabel(c), 12)} ${pad(c.category || '', 10)} ${truncate(c.description, 34)}`);
}
function usage() {
console.log(`TinyEngine 组件查询
用法:
node query_components.mjs list 列出全部组件
node query_components.mjs categories 列出分类
node query_components.mjs cat <分类> 某分类下的组件
node query_components.mjs props <组件名|中文> 某组件的属性表模糊匹配
node query_components.mjs search <关键字> 全字段搜索
示例:
node query_components.mjs props TinyButton
node query_components.mjs props button
node query_components.mjs cat 表单`);
}
function main() {
const [cmd, ...rest] = process.argv.slice(2);
const comps = loadComponents();
switch (cmd) {
case undefined:
case '--help':
case '-h':
return usage();
case 'list':
return listAll(comps);
case 'categories':
case 'cats':
return categories(comps);
case 'cat':
if (!rest[0]) return usage();
return byCategory(comps, rest.join(' '));
case 'props':
if (!rest[0]) return usage();
{
const r = findComp(comps, rest.join(' '));
if (r.comp) return showProps(r.comp);
if (r.multiple) {
console.log(`"${rest.join(' ')}" 匹配到多个组件,请指定更精确的名字:`);
for (const c of r.multiple) console.log(` ${pad(c.component, 22)} ${zhLabel(c)}`);
return;
}
console.log(`未找到组件 "${rest.join(' ')}"。运行 "list" 查看全部,或用 "search <关键字>"。`);
}
return;
case 'search':
if (!rest[0]) return usage();
return search(comps, rest.join(' '));
default:
console.error(`未知命令: ${cmd}`);
return usage();
}
}
main();

View File

@ -1,43 +0,0 @@
#!/bin/bash
# TinyEngine DSL 综合验证脚本
# 运行所有检查结构验证、事件绑定检查、CSS检查
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DSL_FILE="$1"
if [ -z "$DSL_FILE" ]; then
echo "Usage: validate_all.sh <dsl-file>"
exit 1
fi
if [ ! -f "$DSL_FILE" ]; then
echo "❌ File not found: $DSL_FILE"
exit 1
fi
echo "======================================"
echo "TinyEngine DSL 综合验证"
echo "文件: $DSL_FILE"
echo "======================================"
echo
# 1. 结构验证
echo "1⃣ 结构验证..."
node "$SCRIPT_DIR/validate_dsl.mjs" "$DSL_FILE" || exit 1
echo
# 2. 事件绑定检查
echo "2⃣ 事件绑定检查..."
node "$SCRIPT_DIR/check_event_bindings.mjs" "$DSL_FILE" || exit 1
echo
# 3. CSS 语法检查
echo "3⃣ CSS 语法检查..."
node "$SCRIPT_DIR/check_css.mjs" "$DSL_FILE" basic || exit 1
echo
echo "======================================"
echo "✅ 所有验证通过!"
echo "======================================"

View File

@ -1,306 +0,0 @@
#!/usr/bin/env node
/**
* TinyEngine DSL Validator
*
* 验证生成的DSL是否符合TinyEngine协议规范
*
* 零依赖仅用 Node 标准库
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
/** 把 JS 值映射为类型名,用于告警里的 "got: <类型>" 描述。 */
function typeName(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
switch (typeof value) {
case 'string':
return 'string';
case 'boolean':
return 'boolean';
case 'number':
return Number.isInteger(value) ? 'integer' : 'number';
default:
return 'object';
}
}
/** 普通对象判定(非 null、非数组 */
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export class TinyEngineValidator {
/**
* @param {*} dslData DSL 数据
* @param {string} [schemaType='auto'] 'page' | 'block' | 'app' | 'auto'
*/
constructor(dslData, schemaType = 'auto') {
this.original = dslData;
// 落盘文件是"外层包装 + 内层 DSL",这里解包到内层 DSL 再做协议校验。
const [dsl, fromWrapper] = TinyEngineValidator._unwrap(dslData);
this.dsl = dsl;
this._fromWrapper = fromWrapper;
this.schemaType = schemaType;
this.errors = [];
this.warnings = [];
// 自动检测 schema 类型
if (schemaType === 'auto') {
this.schemaType = this._detectSchemaType();
}
}
/**
* 识别并解包外层包装结构
* TinyEngine 落盘的页面/区块文件是"外层包装 + 内层 DSL"结构
* - 页面真正的页面 DSL page_content
* - 区块真正的区块 DSL content
* 返回 [内层DSL, 是否来自包装]若不是包装结构原样返回 [原数据, false]
* 仅当内层确实是含 componentName 的节点时才解包避免误吞同名普通字段
*/
static _unwrap(dslData) {
if (isPlainObject(dslData)) {
for (const key of ['page_content', 'content']) {
const inner = dslData[key];
if (isPlainObject(inner) && 'componentName' in inner) {
return [inner, true];
}
}
}
return [dslData, false];
}
/** 自动检测 schema 类型 */
_detectSchemaType() {
if ('componentName' in this.dsl) {
const cn = this.dsl.componentName;
if (cn === 'Page') return 'page';
if (cn === 'Block') return 'block';
// 其它 componentName 落到 unknown不进入 app 分支)
} else if ('componentsTree' in this.dsl || 'version' in this.dsl) {
return 'app';
}
return 'unknown';
}
/** 验证 DSL返回是否有效 */
validate() {
if (this.schemaType === 'page') return this._validatePage();
if (this.schemaType === 'block') return this._validateBlock();
if (this.schemaType === 'app') return this._validateApp();
this.errors.push(`Unknown schema type: ${this.schemaType}`);
return false;
}
_validatePage() {
for (const field of ['componentName', 'fileName']) {
if (!(field in this.dsl)) this.errors.push(`Missing required field: ${field}`);
}
if (this.dsl.componentName !== 'Page') {
this.errors.push(`Page componentName must be 'Page', got: ${this.dsl.componentName}`);
}
// Page 外层包装的 app 引用必须是字符串pages.js list()/create() 均用 appId.toString()
// 查询;写成数字会导致直接落盘的 page 文件查不到。
if (this._fromWrapper && isPlainObject(this.original)) {
const appRef = this.original.app;
if (appRef !== undefined && typeof appRef !== 'string') {
this.warnings.push(
`Page wrapper 'app' should be string, got: ${typeName(appRef)} (pages.js queries with appId.toString(); numeric app ref won't be found by list())`
);
}
}
// 原始页面协议(IPageSchema)要求 meta但外层包装格式把 meta 等元信息上提到包装层,
// page_content 内不再含 meta。因此仅在校验"裸"页面 DSL 时强制要求 meta。
if ('meta' in this.dsl) {
this._validateMeta(this.dsl.meta);
} else if (!this._fromWrapper) {
this.errors.push('Missing required field: meta');
}
if (this.dsl.children) {
this._validateChildren(this.dsl.children);
}
return this.errors.length === 0;
}
_validateBlock() {
for (const field of ['componentName', 'fileName']) {
if (!(field in this.dsl)) this.errors.push(`Missing required field: ${field}`);
}
if (this.dsl.componentName !== 'Block') {
this.errors.push(`Block componentName must be 'Block', got: ${this.dsl.componentName}`);
}
if ('schema' in this.dsl) {
this._validateBlockSchema(this.dsl.schema);
}
if (this.dsl.children) {
this._validateChildren(this.dsl.children);
}
return this.errors.length === 0;
}
_validateApp() {
for (const field of ['version', 'componentsMap', 'componentsTree']) {
if (!(field in this.dsl)) this.errors.push(`Missing required field: ${field}`);
}
// 验证 app ID 格式
if ('id' in this.dsl) {
const rootId = this.dsl.id;
if (!Number.isInteger(rootId)) {
this.warnings.push(`App Schema 'id' should be integer, got: ${typeName(rootId)} (will be coerced)`);
}
}
// 验证 meta.appId 格式
if ('meta' in this.dsl && 'appId' in this.dsl.meta) {
const appId = this.dsl.meta.appId;
if (!Number.isInteger(appId)) {
this.warnings.push(`meta.appId should be integer, got: ${typeName(appId)} (will be coerced)`);
}
}
// 验证 componentsMap
if ('componentsMap' in this.dsl) {
this._validateComponentsMap(this.dsl.componentsMap);
}
// 验证 componentsTree
if ('componentsTree' in this.dsl) {
for (const page of this.dsl.componentsTree) {
const pageValidator = new TinyEngineValidator(page, 'auto');
if (!pageValidator.validate()) {
const fileName = Object.prototype.hasOwnProperty.call(page, 'fileName') ? page.fileName : '?';
for (const e of pageValidator.errors) {
this.errors.push(`[Page ${fileName}] ${e}`);
}
}
}
}
return this.errors.length === 0;
}
_validateMeta(meta) {
for (const field of ['id', 'title', 'router', 'creator', 'isHome', 'parentId', 'rootElement']) {
if (!(field in meta)) this.errors.push(`Missing meta field: ${field}`);
}
}
_validateComponentsMap(componentsMap) {
for (const comp of componentsMap) {
for (const field of ['componentName', 'package', 'exportName']) {
if (!(field in comp)) this.errors.push(`componentsMap missing field: ${field}`);
}
}
}
_validateBlockSchema(schema) {
if ('properties' in schema) {
for (const prop of schema.properties) {
if (!('content' in prop)) this.errors.push("Block schema property missing 'content'");
}
}
}
_validateChildren(children) {
// children 可以是字符串(文本子节点),按协议(IComponentSchema[] | string)
// 这是合法形态,无需逐项校验;递归调用遇到字符串时同样直接返回。
if (typeof children === 'string') return;
children.forEach((child, i) => {
if (!isPlainObject(child)) {
this.errors.push(`Child at index ${i} is not an object`);
return;
}
if (!('componentName' in child)) {
this.errors.push(`Child at index ${i} missing componentName`);
}
// 检查 ID
if (!('id' in child)) {
this.warnings.push(`Child at index ${i} missing id (recommended)`);
}
// 检查 props 中是否有错误的 'class' 字段(应该是 'className'
if ('props' in child && isPlainObject(child.props)) {
if ('class' in child.props) {
const component = Object.prototype.hasOwnProperty.call(child, 'componentName') ? child.componentName : 'unknown';
this.errors.push(
`${component} at index ${i} uses 'class' in props, should use 'className' instead (React/Vue convention)`
);
}
}
// 验证嵌套 children
if (child.children) {
this._validateChildren(child.children);
}
});
}
/** 生成验证报告 */
report() {
const lines = [];
if (this.errors.length) {
lines.push('❌ Validation Errors:');
for (const error of this.errors) lines.push(` - ${error}`);
}
if (this.warnings.length) {
lines.push('⚠️ Warnings:');
for (const warning of this.warnings) lines.push(` - ${warning}`);
}
if (this.errors.length === 0 && this.warnings.length === 0) {
lines.push('✅ Validation passed!');
}
return lines.join('\n');
}
}
function main() {
if (process.argv.length < 3) {
console.log('Usage: validate_dsl.mjs <dsl-file> [schema-type]');
console.log(' schema-type: page, block, app, or auto (default)');
process.exit(1);
}
const filePath = process.argv[2];
const schemaType = process.argv[3] || 'auto';
let dslData;
try {
const text = fs.readFileSync(filePath, 'utf8');
dslData = JSON.parse(text);
} catch (e) {
if (e instanceof SyntaxError) {
console.log(`❌ Invalid JSON: ${e.message}`);
} else if (e.code === 'ENOENT') {
console.log(`❌ File not found: ${filePath}`);
} else {
throw e;
}
process.exit(1);
}
const validator = new TinyEngineValidator(dslData, schemaType);
const isValid = validator.validate();
console.log(validator.report());
process.exit(isValid ? 0 : 1);
}
const __filename = fileURLToPath(import.meta.url);
if (path.resolve(process.argv[1] || '') === __filename) {
main();
}

View File

@ -1,220 +0,0 @@
#!/usr/bin/env node
/**
* TinyEngine Page DSL 综合验证
*
* 验证包装格式的页面DSL文件包含 name, id, app, route, page_content 等字段
* 运行所有检查结构验证事件绑定检查CSS检查
*
* 零依赖仅用 Node 标准库
* 通过子进程调用同目录下的 check_event_bindings.mjs / check_css.mjs子进程直接继承 stdout/stderr
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
/** 普通对象判定(非 null、非数组 */
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/** 把 JS 值映射为类型名,用于告警里的 "got: <类型>" 描述。 */
function typeName(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
switch (typeof value) {
case 'string':
return 'string';
case 'boolean':
return 'boolean';
case 'number':
return Number.isInteger(value) ? 'integer' : 'number';
default:
return 'object';
}
}
/** 读取并解析 JSON 文件;失败时抛出 SyntaxErrorJSON或带 code 的系统错误(文件)。 */
function readJson(filePath) {
const text = fs.readFileSync(filePath, 'utf8');
return JSON.parse(text);
}
/** 验证包装格式的页面文件 */
function validatePageWrapper(filePath) {
console.log(`验证文件: ${filePath}`);
console.log('='.repeat(50));
let data;
try {
data = readJson(filePath);
} catch (e) {
if (e instanceof SyntaxError) {
console.log(`❌ Invalid JSON: ${e.message}`);
} else if (e.code === 'ENOENT') {
console.log(`❌ File not found: ${filePath}`);
} else {
throw e;
}
return false;
}
// 检查是否是包装格式
if (!('page_content' in data)) {
console.log('❌ 不是有效的页面文件(缺少 page_content 字段)');
return false;
}
const pageContent = data.page_content;
// 检查必要字段
for (const field of ['name', 'id', 'app', 'route']) {
if (!(field in data)) {
console.log(`❌ 缺少必要字段: ${field}`);
return false;
}
}
// 验证 app 字段格式:页面外层 app 引用必须是字符串。
// pages.js list()/create() 均用 appId.toString() 查询,数字 app 会导致直接落盘的页面查不到。
if ('app' in data) {
const appField = data.app;
if (typeof appField !== 'string') {
console.log(
`⚠️ WARNING: 'app' field should be string, got: ${typeName(appField)} (pages.js queries with appId.toString(); numeric app ref won't be found by list())`
);
}
}
// 检查 page_content 中的必要字段
if (!('componentName' in pageContent)) {
console.log('❌ page_content 缺少 componentName 字段');
return false;
}
if (pageContent.componentName !== 'Page') {
console.log(`❌ componentName 必须是 'Page',实际是: ${pageContent.componentName}`);
return false;
}
if (!('fileName' in pageContent)) {
console.log('❌ page_content 缺少 fileName 字段');
return false;
}
console.log('✅ 包装格式检查通过');
return true;
}
/** 运行所有检查器 */
function runCheckers(filePath) {
// 1. 事件绑定检查
console.log('\n1⃣ 事件绑定检查...');
const eventResult = spawnSync(
process.execPath,
[path.join(SCRIPT_DIR, 'check_event_bindings.mjs'), filePath],
{ stdio: 'inherit' }
);
if (eventResult.status !== 0) return false;
// 2. CSS 检查
console.log('\n2⃣ CSS 语法检查...');
const cssResult = spawnSync(
process.execPath,
[path.join(SCRIPT_DIR, 'check_css.mjs'), filePath, 'basic'],
{ stdio: 'inherit' }
);
if (cssResult.status !== 0) return false;
return true;
}
/** 检查是否正确使用 className 而不是 class */
function checkClassNameUsage(filePath) {
let data;
try {
data = readJson(filePath);
} catch (e) {
// JSON 语法错误或文件读取问题(如 ENOENT由其他检查负责报告
// 此处不掩盖其他意外运行错误。
if (e instanceof SyntaxError || (e && typeof e.code === 'string')) {
return true;
}
console.log(`❌ className 检查异常: ${e}`);
return false;
}
const pageContent = Object.prototype.hasOwnProperty.call(data, 'page_content')
? data.page_content
: {};
const errors = [];
const checkNode = (node) => {
if (isPlainObject(node)) {
// 检查 props 中是否有 'class'
if ('props' in node && isPlainObject(node.props)) {
if ('class' in node.props) {
const component = Object.prototype.hasOwnProperty.call(node, 'componentName')
? node.componentName
: 'unknown';
errors.push(`${component} 使用了 'class' 而不是 'className'`);
}
}
// 递归检查 children
if (Array.isArray(node.children)) {
for (const child of node.children) checkNode(child);
}
} else if (Array.isArray(node)) {
for (const item of node) checkNode(item);
}
};
checkNode(pageContent);
if (errors.length) {
console.log("\n❌ 发现错误的 'class' 使用(应该使用 'className':");
for (const error of errors) console.log(` - ${error}`);
return false;
}
console.log('\n✅ className 检查通过');
return true;
}
function main() {
if (process.argv.length < 3) {
console.log('Usage: validate_page.mjs <page-file>');
console.log('验证包装格式的TinyEngine页面DSL文件');
process.exit(1);
}
const filePath = process.argv[2];
// 运行所有检查
let allPassed = true;
// 1. 包装格式检查
if (!validatePageWrapper(filePath)) allPassed = false;
// 2. className 检查
if (!checkClassNameUsage(filePath)) allPassed = false;
// 3. 运行其他检查器
if (!runCheckers(filePath)) allPassed = false;
// 总结
console.log('\n' + '='.repeat(50));
if (allPassed) {
console.log('✅ 所有验证通过!');
} else {
console.log('❌ 验证失败,请修复错误后重试');
}
process.exit(allPassed ? 0 : 1);
}
main();

View File

@ -1,11 +0,0 @@
version: 2.0
steps:
pre_codecheck:
- checkout
tool_params:
secsolar:
source_dir: ./
cmetrics:
exclude: vite.config.js|package.json|index.js|axios.js|.eslintrc.js|mockServer|packages/engine-cli/template|packages/vue-generator/test|packages/vue-generator/src/templates|packages/build/vite-plugin-meta-comments/src/test

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
*.conf
pnpm-lock.yaml

View File

@ -1,7 +0,0 @@
SQL_HOST=localhost
SQL_PORT=3306
SQL_USER=root
SQL_PASSWORD=admin
SQL_DATABASE=tiny_engine
backend_url=http://localhost:9090/material-center/api/component/bundle/create

View File

@ -1,5 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Questions or need help
url: https://github.com/opentiny/tiny-engine/discussions
about: Add this WeChat(opentiny-official), we will invite you to the WeChat discussion group later.

View File

@ -1,31 +0,0 @@
name: ✨ Feature Request
description: Propose new features to @opentiny/tiny-engine to improve it.
title: '✨ [Feature]: '
labels: ['✨ feature']
body:
- type: textarea
id: feature-solve
attributes:
label: What problem does this feature solve
description: |
Explain your use case, context, and rationale behind this feature request. More importantly, what is the end user experience you are trying to build that led to the need for this feature?
placeholder: Please Input
validations:
required: true
- type: textarea
id: feature-api
attributes:
label: What does the proposed API look like
description: |
Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use Markdown to format your code blocks.
placeholder: Please Input
validations:
required: true
- type: input
id: project-name
attributes:
label: What is your project name
description: We also welcome you to fill in more detailed project information in the following issue [#334](https://github.com/opentiny/tiny-engine/issues/334).
placeholder: Please Input
validations:
required: true

View File

@ -1,56 +0,0 @@
version: v1
labels:
- label: 'ignore-for-release'
sync: true
matcher:
title: '^[vV]?\d+(\.\d+){2}.*'
body: '#ignore'
- label: 'breaking-change'
sync: true
matcher:
title: '^.+!:|#breaking'
body: '## Does this PR introduce a breaking change\?\s+- \[[xX]\] *Yes'
- label: 'enhancement'
sync: true
matcher:
title: '^[fF]eat.*'
- label: 'bug'
sync: true
matcher:
title: '^[fF]ix.*'
- label: 'documentation'
sync: true
matcher:
title: '^[dD](ocs|ocumentation).*'
files: 'docs/**/*.md'
- label: 'refactoring'
sync: true
matcher:
title: '^[rR]efactor.*'
- label: 'test'
sync: true
matcher:
title: '^[tT]est.*'
files: '**/__tests__/**'
- label: 'chore'
sync: true
matcher:
title: '^[cC]hore.*'
- label: 'ci'
sync: true
matcher:
title: '^[cC]i.*'
files: '.github/**'
- label: 'ospp'
sync: true
matcher:
baseBranch: '^ospp-\d+\/.*'
- label: '1.x'
sync: true
matcher:
baseBranch: '^v1\.x'
- label: 'release'
sync: true
matcher:
baseBranch: '^release\/v2.*'

31
.github/release.yml vendored
View File

@ -1,31 +0,0 @@
changelog:
exclude:
labels:
- ignore-for-release
categories:
- title: Breaking Changes 🛠
labels:
- Semver-Major
- breaking-change
- title: Exciting New Features 🎉
labels:
- Semver-Minor
- feature
- enhancement
- title: Bug Fixes 🐛
labels:
- Semver-Patch
- bug
- title: "📖 Documentation"
labels:
- documentation
- title: "🔧 Maintenance"
labels:
- refactoring
- test
- unit-test
- chore
- ci
- title: "Other Changes"
labels: ["*"]
collapse: true

View File

@ -1,76 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
if: github.repository == 'opentiny/tiny-engine'
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install
- name: Run Build
run: pnpm run build:plugin && pnpm run build:alpha > /tmp/build-alpha.log 2>&1
- name: Upload build logs
uses: actions/upload-artifact@v4
with:
name: build-alpha-log
path: /tmp/build-alpha.log
- name: Parse Publish tag
id: parse_tag
run: |
tag_name="${GITHUB_REF#refs/tags/}"
if [[ "$tag_name" == *alpha* ]]; then
echo "dist_tag=alpha" >> "$GITHUB_OUTPUT"
elif [[ "$tag_name" == *beta* ]]; then
echo "dist_tag=beta" >> "$GITHUB_OUTPUT"
elif [[ "$tag_name" == *rc* ]]; then
echo "dist_tag=rc" >> "$GITHUB_OUTPUT"
else
echo "dist_tag=latest" >> "$GITHUB_OUTPUT"
fi
- name: Verify clean working directory
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "Working directory is not clean"
exit 1
fi
- name: Verify package version match tag
run: |
tag_name="${GITHUB_REF#refs/tags/}"
package_version=$(pnpm lerna list --scope=@opentiny/tiny-engine --json | jq -r '.[0].version')
if [[ "$tag_name" != "v$package_version" ]]; then
echo "Tag name $tag_name does not match package version $package_version"
exit 1
fi
- name: Publish package to npm
run: pnpm lerna publish from-package --dist-tag ${{steps.parse_tag.outputs.dist_tag}} --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,19 +0,0 @@
name: Pull Request Auto Labeler
on:
pull_request_target:
types: [opened, edited]
permissions:
# Setting up permissions in the workflow to limit the scope of what it can do. Optional!
contents: read # the config file
pull-requests: write # for labeling pull requests (on: pull_request_target or on: pull_request)
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: fuxingloh/multi-labeler@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }} # optional, default to '${{ github.token }}'
config-path: .github/auto-labeler.yml # optional, default to '.github/labeler.yml'

View File

@ -1,162 +0,0 @@
name: Deploy to CDN
on:
workflow_dispatch:
inputs:
is_latest_release:
description: '当前分支是否是最新release版本'
required: true
default: true
type: boolean
jobs:
check-secrets:
runs-on: ubuntu-latest
outputs:
secrets-ready: ${{ steps.check.outputs.secrets-ready }}
steps:
- name: Check required secrets
id: check
run: |
if [[ -z "${{ secrets.HUAWEI_CLOUD_AK }}" ]] || \
[[ -z "${{ secrets.HUAWEI_CLOUD_SK }}" ]] || \
[[ -z "${{ secrets.HUAWEI_CLOUD_ENDPOINT }}" ]] || \
[[ -z "${{ secrets.HUAWEI_CLOUD_BUCKET }}" ]]; then
echo "secrets-ready=false" >> $GITHUB_OUTPUT
echo "::error::Required Huawei Cloud secrets are not configured."
echo "::error::Please set: HUAWEI_CLOUD_AK, HUAWEI_CLOUD_SK, HUAWEI_CLOUD_ENDPOINT, HUAWEI_CLOUD_BUCKET"
exit 1
fi
echo "secrets-ready=true" >> $GITHUB_OUTPUT
echo "✅ All required secrets are configured"
build:
runs-on: ubuntu-latest
needs: check-secrets
outputs:
version-timestamp: ${{ steps.prepare-version.outputs.version_timestamp }}
cdn-base: ${{ steps.prepare-version.outputs.cdn_base }}
cdn-base-latest: ${{ steps.prepare-version.outputs.cdn_base_latest }}
obs-path: ${{ steps.prepare-version.outputs.obs_path }}
obs-path-latest: ${{ steps.prepare-version.outputs.obs_path_latest }}
concurrency:
group: deploy-cdn
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install
- name: add environment variable
run: |
VITE_ORIGIN_URL="https://agent.opentiny.design/"
cat <<EOF >> designer-demo/env/.env.alpha
# ---- appended by CI (deploy-cdn) ----
VITE_ORIGIN=$VITE_ORIGIN_URL
EOF
echo "VITE_ORIGIN_URL=$VITE_ORIGIN_URL"
- id: prepare-version
name: Prepare version-timestamp
run: |
# Extract version from package.json
VERSION=$(node -p "require('./designer-demo/package.json').version")
# Generate timestamp in YYYYMMDD-HHMMSS format
TIMESTAMP=$(TZ="Asia/Shanghai" date +%Y%m%d-%H%M%S)
# Combine for version-timestamp
VERSION_TIMESTAMP="${VERSION}-${TIMESTAMP}"
# Set CDN base path
CDN_BASE="https://res-static.opentiny.design/tiny-engine-designer/${VERSION_TIMESTAMP}/"
CDN_BASE_LATEST="https://res-static.opentiny.design/tiny-engine-designer/latest/"
OBS_PATH="tiny-engine-designer/${VERSION_TIMESTAMP}/"
OBS_PATH_LATEST="tiny-engine-designer/latest"
# Export as environment variables for subsequent steps
echo "VERSION_TIMESTAMP=$VERSION_TIMESTAMP" >> $GITHUB_ENV
echo "CDN_BASE=$CDN_BASE" >> $GITHUB_ENV
echo "OBS_PATH=$OBS_PATH" >> $GITHUB_ENV
# Set outputs for job-level export
echo "version_timestamp=$VERSION_TIMESTAMP" >> $GITHUB_OUTPUT
echo "cdn_base=$CDN_BASE" >> $GITHUB_OUTPUT
echo "cdn_base_latest=$CDN_BASE_LATEST" >> $GITHUB_OUTPUT
echo "obs_path=$OBS_PATH" >> $GITHUB_OUTPUT
echo "obs_path_latest=$OBS_PATH_LATEST" >> $GITHUB_OUTPUT
echo "Version-Timestamp: $VERSION_TIMESTAMP"
echo "CDN Base: $CDN_BASE"
echo "OBS Path: $OBS_PATH"
- name: Run Build
run: |
set -eo pipefail
pnpm run build:plugin 2>&1 | tee /tmp/build-plugin.log
# Run build:alpha equivalent with --base parameter
pnpm run build:alpha --base=${{ env.CDN_BASE }} 2>&1 | tee /tmp/build-alpha.log
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: designer-demo-dist
path: ./designer-demo/dist/
retention-days: 1
deploy-cdn:
runs-on: ubuntu-latest
needs: [check-secrets, build]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: designer-demo-dist
path: ./designer-demo/dist/
- name: Install obsutil
run: |
curl -o obsutil.tar.gz https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_linux_amd64.tar.gz
tar -xzf obsutil.tar.gz
chmod +x obsutil_linux_amd64_*/obsutil
sudo mv obsutil_linux_amd64_*/obsutil /usr/local/bin/obsutil
- name: Configure and Upload to OBS
run: |
obsutil config -i=${{ secrets.HUAWEI_CLOUD_AK }} \
-k=${{ secrets.HUAWEI_CLOUD_SK }} \
-e=${{ secrets.HUAWEI_CLOUD_ENDPOINT }}
# Upload to versioned path
obsutil cp ./designer-demo/dist \
obs://${{ secrets.HUAWEI_CLOUD_BUCKET }}/${{ needs.build.outputs.obs-path }} \
-r -f -flat
# If is_latest_release is true, also upload to latest path
if [ "${{ github.event.inputs.is_latest_release }}" = "true" ]; then
# use cdn-base-latest replace cdn-base in all ./designer-demo/dist files
find ./designer-demo/dist -type f \( -name "*.html" -o -name "*.js" -o -name "*.mjs" -o -name "*.css" \) \
-exec sed -i "s|${{ needs.build.outputs.cdn-base }}|${{ needs.build.outputs.cdn-base-latest }}|g" {} +
obsutil cp ./designer-demo/dist \
obs://${{ secrets.HUAWEI_CLOUD_BUCKET }}/${{ needs.build.outputs.obs-path-latest }} \
-r -f -flat
fi
echo "Uploaded to: obs://${{ secrets.HUAWEI_CLOUD_BUCKET }}/${{ needs.build.outputs.obs-path }}"
echo "CDN URL: https://res-static.opentiny.design/${{ needs.build.outputs.obs-path }}"

View File

@ -1,74 +0,0 @@
name: Deploy to GitHub Pages
on:
push:
branches: [develop]
workflow_dispatch:
inputs:
backend_url:
description: 'Backend Base URL (e.g. https://agent.bytedev.site/)'
required: true
default: 'https://agent.bytedev.site/'
type: string
jobs:
deploy-gh-pages:
runs-on: ubuntu-latest
concurrency:
group: deploy-gh-pages
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install
- name: add environment variable
env:
ENV_BACKEND_URL: ${{ github.event.inputs.backend_url || 'https://agent.bytedev.site/' }}
run: |
if [[ "$ENV_BACKEND_URL" == *$'\n'* || "$ENV_BACKEND_URL" == *$'\r'* ]]; then
echo "::error::backend_url must be a single-line value"
exit 1
fi
if [[ ! "$ENV_BACKEND_URL" =~ ^https?://[a-zA-Z0-9._-]+(:[0-9]+)?(/[a-zA-Z0-9._/~:%-]*)?$ ]]; then
echo "::error::backend_url must be an http(s) base URL using only URL-safe characters (letters, digits, . _ - / ~ : %)"
exit 1
fi
ENV_BACKEND_URL="${ENV_BACKEND_URL%/}/"
cat <<EOF >> designer-demo/env/.env.alpha
# ---- appended by CI (gh-pages) ----
VITE_ORIGIN=$ENV_BACKEND_URL
EOF
echo "VITE_ORIGIN_URL=$ENV_BACKEND_URL"
- name: Run Build
run: |
set -eo pipefail
pnpm run build:plugin 2>&1 | tee /tmp/build-plugin.log
pnpm run build:alpha 2>&1 | tee /tmp/build-alpha.log
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./designer-demo/dist/
keep_files: true
force_orphan: false
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'

79
.github/workflows/docker-dry-run.yaml vendored Normal file
View File

@ -0,0 +1,79 @@
name: Build And Publish (Dry run)
on:
workflow_dispatch:
jobs:
front:
name: Build front-end docker image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
gaonengwww/dl-flow-frontend
tags: |
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
working-directory: ./
context: ./
build-context: ./
file: ./dockerfile
push: false
back:
name: Build back-end docker image
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./dl-flow-backend
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
gaonengwww/dl-flow-backend
tags: |
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
working-directory: dl-flow-backend/
context: dl-flow-backend/
build-context: dl-flow-backend/
file: dl-flow-backend/dockerfile
push: false

92
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,92 @@
name: Build And Publish
on:
push:
tags:
- "*.*.*"
jobs:
front:
name: Build Front docker image
runs-on: ubuntu-latest
defaults:
run:
working-directory: .
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
gaonengwww/dl-flow-frontend
tags: |
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
working-directory: ./
context: ./
build-context: ./
file: ./dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
back:
name: Build docker image
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./dl-flow-backend
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
gaonengwww/dl-flow-backend
tags: |
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
working-directory: dl-flow-backend/
context: dl-flow-backend/
build-context: dl-flow-backend/
file: dl-flow-backend/dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,43 +0,0 @@
name: Push And Create PR Check
on:
push:
branches: []
pull_request:
branches: [develop, main, refactor/develop, release/*]
jobs:
push-check:
runs-on: ubuntu-latest # windows-latest || macos-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: pnpm i
- name: Get changed files
id: get_changed_files
uses: tj-actions/changed-files@v41
with:
files: |
**.js
**.vue
**.jsx
- name: Run ESLint
run: npx eslint ${{steps.get_changed_files.outputs.all_changed_files}}
- name: Run Build
run: pnpm run build:plugin && pnpm run build:alpha > build-alpha.log 2>&1
- name: Upload build logs
uses: actions/upload-artifact@v4
with:
name: build-alpha-log
path: build-alpha.log

36
.gitignore vendored
View File

@ -1,34 +1,2 @@
.DS_Store
node_modules
dist/
package-lock.json
yarn.lock
pnpm-lock.yaml
lerna-debug.log
packages/design-core/bundle-deps
designer-demo/bundle-deps
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
tmp
temp
__pycache__
# .claude/skills is a generated link to .agents/skills (see scripts/link-skills.js)
.claude/skills
node_modules
**/node_modules

View File

@ -1,5 +0,0 @@
dist
package-lock.json
**/node_modules/**
# 忽略该文件夹下的测试对比文件,防止自动去掉分号之后导致测试失败
packages/build/vite-plugin-meta-comments/test/expected/**

View File

@ -1,7 +0,0 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 120,
"trailingComma": "none",
"endOfLine": "auto"
}

120
AGENTS.md
View File

@ -1,120 +0,0 @@
# TinyEngine — Repository Instructions for Coding Agents
## Purpose and Scope
This file is the canonical source of truth for repo-wide agent instructions.
- Applies to the whole repository unless a closer `AGENTS.md` overrides it for a subtree.
- `CLAUDE.md` is a compatibility entrypoint that imports this file. Do not maintain a second independent copy of the same rules.
- Keep this file limited to repo-wide guidance. Package-specific implementation details belong in package-level instruction files.
## Repository Snapshot
- Monorepo: pnpm workspaces + lerna (independent versioning)
- Primary stack: Vue 3, Vite, JavaScript/TypeScript
- Package manager: `pnpm` only for interactive work in this repo
- Designer app: `designer-demo/`
- Local mock backend: `mockServer/`
## Working Model
- Inspect the affected package, its `package.json`, and the nearest instruction file before editing.
- Keep changes scoped. Do not normalize unrelated files or rename fixtures just for consistency.
- Prefer targeted package-level validation over whole-repo commands when possible.
- Treat `pnpm lint` and `pnpm format` as mutating commands, not read-only verification.
- Do not invoke `npm` or `yarn` directly for normal repo work. Existing package scripts may still shell out internally; leave that alone unless the task is specifically about package scripts.
## Common Commands
### Read-mostly commands
```sh
pnpm install
pnpm dev
pnpm build:plugin
pnpm build:alpha
pnpm --filter @opentiny/tiny-engine-dsl-vue test:unit
```
### Mutating commands
```sh
pnpm lint # ESLint with --fix
pnpm format # Prettier --write
```
Canonical script definitions live in:
- `package.json`
- `packages/*/package.json`
- `.github/workflows/push-check.yml`
- `.github/workflows/Release.yml`
## Verification Matrix
Run the smallest sufficient verification for the change surface, then expand if the change is broad or risky.
1. Docs-only changes:
No code verification required unless the docs change commands or workflow descriptions that should be checked against source files.
2. `packages/vue-generator/**`:
Run the affected testcase or `pnpm --filter @opentiny/tiny-engine-dsl-vue test:unit`.
If generator behavior changes, run the full `test:unit` suite before handoff and inspect any changed `expected/*.vue` files.
3. Published library packages under `packages/**`:
Run the package-local `test` script if one exists.
Run `pnpm build:plugin` when build output or published package behavior may be affected.
4. `designer-demo/**` or shared packages consumed by the demo:
Run `pnpm build:alpha`.
5. Cross-package build or release-facing changes:
Run `pnpm build:plugin` and `pnpm build:alpha`.
6. Config, workspace, CI, or release script changes:
Verify the directly affected command(s) after approval.
## Approval Boundaries
### Always OK
- Read any source file
- Run targeted tests and builds
- Edit implementation files inside existing packages
- Add or update tests that match the scope of the change
- Update docs that reflect current repo behavior
### Ask First
- Changing workspace, lerna, pnpm, ESLint, Prettier, or TypeScript configuration
- Modifying CI workflows, release scripts, or publish flows
- Upgrading major dependencies or changing pinned overrides
- Reordering or adding/removing default vue-generator attribute hooks
- Large-scale edits to generated mappings or vendored patches
When asking first, include:
- what you want to change
- why the current rules or implementation are insufficient
- what verification you would run after approval
### Never
- Use `npm` or `yarn` directly for routine repo commands
- Skip hooks with `--no-verify`
- Hardcode versions for workspace packages
- Edit `patches/` without understanding the upstream issue and the patch purpose
- Rewrite generated expectations or snapshots without validating the new output first
## Task-Specific Expectations
- Bug fix:
Add or update a regression test when behavior changes.
- Refactor:
Preserve behavior and prove it with targeted verification.
- Snapshot or generated output change:
Explain why the output changed and list the affected fixture directories.
- Commit or PR work:
Only do it if asked. Use Conventional Commits and target `develop` unless the user specifies otherwise.
## Gotchas
- `pnpm install` is enforced by `preinstall`; npm and yarn are rejected for direct repo usage.
- `pnpm lint` writes fixes. Use it deliberately.
- CI relies on `build:plugin` and `build:alpha`, not only lint or unit tests.
- Test directories such as `test/`, `expected/`, and `output/` are not always linted; do not treat lint success as fixture validation.

View File

@ -1,7 +0,0 @@
# TinyEngine — Claude Code Entry
This file is intentionally thin. The canonical repo-wide instructions live in `AGENTS.md`.
@./AGENTS.md
When working inside a subtree that has its own `CLAUDE.md`, follow the closer file as an extension of these repo-wide rules.

View File

@ -1,13 +0,0 @@
FROM node:18-alpine as build
WORKDIR /app/
ADD . .
RUN npm config set strict-ssl false \
&& npm config set registry https://registry.npmmirror.com/ \
&& npm install pnpm -g \
&& pnpm i \
&& pnpm build:plugin \
&& pnpm build:prod
FROM nginx:latest
COPY --from=build /app/designer-demo/dist/ /usr/share/nginx/html/

402
README.md
View File

@ -1,131 +1,271 @@
<p align="center">
<a href="https://opentiny.design/tiny-engine" target="_blank" rel="noopener noreferrer">
<img alt="OpenTiny Logo" src="logo.svg" height="100" style="max-width:100%;">
</a>
</p>
<p align="center">TinyEngine enables developers to customize low-code platforms, build low-code platforms online in real time, and support secondary development or integration of low-code platform capabilities.</p>
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/opentiny/tiny-engine)
English | [简体中文](README.zh-CN.md)
🌈 Features:
- Cross-end cross-frame front-end components
- Supports online real-time construction, secondary development, or being integrated.
- Directly generate deployable source code without engine support.
- Allows access to third-party components and customized extension plug-ins.
- Supports high-code and low-code, and hybrid development and deployment of applications.
- The platform accesses LLM capabilities to help developers build applications.
## Documentation
- introhttps://opentiny.design/tiny-engine#/home
- tutorialhttps://opentiny.design/tiny-engine#/help-center/index
- playgroundhttps://opentiny.design/tiny-engine#/tiny-engine-editor
## Usage
### Environment Setup
- Install Node.js 18+
- Install pnpm 9+
```sh
$ npm install -g pnpm
```
### Create Low-Code Platform using CLI
```sh
# Create low-code platform
$ npx @opentiny/tiny-engine-cli@latest create-platform <name>
# Enter the low-code platform directory
$ cd <name>
# Install dependencies
$ pnpm install
```
### Local development: Start the local mock server and use the mock data of the local mock server.
> The mock server included with the created low-code platform only provides basic backend mock functionality. If you need to experience the complete backend service capabilities, please refer to the Java backend startup instructions below.
```sh
$ pnpm dev
```
### Local Development with Java Backend
Java backend repository: https://github.com/opentiny/tiny-engine-backend-java
Start Java backend for frontend-backend integration:
[Frontend-Backend Integration Documentation](https://docs.opentiny.design/tiny-engine/dev/debugging-of-java-backend)
### Materials Synchronization [Solution](https://docs.opentiny.design/tiny-engine/dev/material-sync-solution)
```sh
$ pnpm splitMaterials
```
```sh
$ pnpm buildMaterials
```
Open a browser: `http://localhost:8080/?type=app&id=1&tenant=1&pageid=1`
`url search` Parameters:
- `type=app` Application type
- `id=xxx` Application ID
- `tenant=xxx` Organization ID
- `pageid=xxx` Page ID
## Build
```sh
# Build Designer
pnpm run build:alpha or build:prod
```
## Milestones
```mermaid
gantt
dateFormat YYYY-MM-DD
axisFormat %Y-%m-%d
1.0.0-beta.x version : 2023-09-25, 2024-05-20
1.0.0-rc version(refactor version) : 2024-10-01
1.0.0 version : 2024-11-01
2.0.0 version : 2024-12-16
2.1.0 version : 2025-01-02
2.2.0 version : 2025-02-19
2.3.0 version : 2025-03-14
2.4.0 version : 2025-04-07
2.5.0 version : 2025-05-15
```
## 🤝 Participation and Contribution
If you are interested in our open source project, please join us! 🎉
Please read the [Contribution Guide](CONTRIBUTING.md) before participating in the contribution.
- Add official assistant WeChat opentiny-official and join the technical exchange group
- Join the mailing list opentiny@googlegroups.com
## ❤️ Acknowledgments
Thanks to all the developers who have contributed to TinyEngine!
<p align="center">
<a href="https://github.com/opentiny/tiny-engine/graphs/contributors" target="_blank">
<img alt="Contributors" src="https://contrib.rocks/image?repo=opentiny/tiny-engine">
</a>
</p>
## License
[MIT](LICENSE)
# dl-flow
dl-Flow 是一种拖拽式的线性网络搭建的 Web 应用程序。你可以使用该程序直观的搭建一个paddlepaddle的神经网络.
## Quick Start
### Docker 搭建
我们非常推荐您使用Docker进行部署. 这不仅可以让你快速的进行上手, 也可以让您再后续对接其他程序更加的方便快捷(例如 K8s).
```yaml
# docker-compose.yaml
services:
mongodb:
image: mongo
ports:
- 27018:27017
redis:
image: redis
ports:
- 6379:6379
front:
image: gaonengwww/dl-flow-frontend
ports:
- 80:80
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
server:
image: gaonengwww/dl-flow-backend
ports:
- 9000:9000
environment:
- DB_URL=mongodb://mongodb:27017/dl-flow # 数据库地址
- REDIS_HOST=redis # redis地址 (必填)
- REDIS_PORT=6379 # redis端口 (必填)
- REDIS_DB=0 # redis数据库 (必填)
- REDIS_PASSWORD="" # redis密码
- JWT_EXPIRE_IN=1d # JWT 过期时间 (必填)
- JWT_SIGN_ALGORITHM=RS256 # JWT签名算法, 要与密钥对符合, 例如密钥对是RSA 2048bit, 那么此处应该是 RS256 (必填)
- JWT_PUB_KEY=./keys/pub.key # JWT 公钥 (必填)
- JWT_PRI_KEY=./keys/pri.key # JWT 私钥 (必填)
- PWD_SALT=salt # bcrypt 盐(必填)
- PWD_SALT_LEN=12 # bcrypt 盐(必填)
volumes: # 强烈将下述卷挂载到本地, 以避免数据丢失
- ./public:/public # 代码生成暂存位置
- ./keys:/keys # 密钥对存放位置
- ./data:/data # bundle.json与install.lock 存放位置
- ./examples:/examples # 示例文件夹
```
`Web-Ui` 使用nginx驱动, 接下来我们需要编写 `nginx.conf`
```conf
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
gzip on;
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location ~ /endpoint/ {
rewrite ^/endpoint/(.*)$ /$1 break; # 主要负责反代的rewrite, 否则发的是 http://server:9000/endpoint/
proxy_pass http://server:9000;
}
location ~ /socket.io {
proxy_pass http://server:9001;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}
```
```bash
docker compose up -d
```
接下来,您便可以访问 `http://localhost` 开始搭建
## 源码构建
```bash
git clone https://atomgit.com/opentiny/000003.git
# git clone git@atomgit.com:opentiny/000003.git
ls -al
# .github
# dl-flow-backend // 后端
# dl-flow-frontend // WebUi
# dl-flow-example // 示例
# nginx.conf // 预设好的nginx文件
```
### 后端构建
后端采用的是`Nest.js`
```bash
cd dl-flow-backend
pnpm install
pnpm build
node dist/main.js
```
### 前端构建
前端魔改自`TinyEngine`, 部署方式与`TinyEngine`大同小异.
```bash
cd dl-flow-frontend
pnpm install
pnpm build:plugin
pnpm build:prod
cd packages/design-core/dist
```
### 环境变量与含义
- DB_URL: 数据库链接地址 (必填)
- REDIS_HOST: redis地址 (必填)
- REDIS_PORT: redis端口 (必填)
- REDIS_DB: redis数据库 (必填)
- REDIS_PASSWORD: redis密码 (必填)
- JWT_EXPIRE_IN: JWT过期时间, 规则可参考[vercel/ms](https://github.com/vercel/ms) (必填)
- JWT_SIGN_ALGORITHM: JWT签名算法, 要与密钥对符合, 例如密钥对是RSA 2048bit, 那么此处应该是 RS256 (必填)
- JWT_PUB_KEY: JWT 公钥 (必填)
- JWT_PRI_KEY: JWT 私钥 (必填)
- ~~PWD_SALT: bcrypt 盐 (必填)~~
- PWD_SALT_LEN: bcrypt 盐长度
### Bug 反馈
如有bug与其他方面的疑问, 欢迎提交[issue](https://atomgit.com/opentiny/000003/issues)
## WebUi布局
![](public/Snipaste_2024-03-24_21-37-53.png)
绿色区域: 网络与layer选择区域
红色区域: 布局区域, 在绿色区域 单击需要的网络后会显示在红色布局区内
蓝色区域: 网络区域, 可以配置网络的一些属性(Covn2D 举例)
- 输入特征大小
- 输出特征大小
- 卷积核大小等
注意, 您应当自行校验网络的配置项, 例如: 理论上卷积核大小为1是可以存在的它也的确有一些作用比如
- 升维/降维
- 增加非线性
但是**可以**这么做并不意味着它**适合**你的需求更不等同于**不存在运行时候错误**(比如维度错误)
## 结构
### 流程图
### Web UI
下图展示了项目与TinyEngine的差异文件
```
├── packages
   ├── canvas
   │   ├── src
   │   │   ├── components
   │   │   │   ├── container
   │   │   │   │   ├── AlgoNode.vue // 创建的自定义节点
   │   │   │   │   ├── GroupNode.vue // 组节点
   │   │   │   │   └── X6Canvas.vue // x6容器
   ├── controller
   │   ├── src
   │   │   ├── useLayer.js // 自定义Layer的逻辑
   │   │   ├── useResource.js // 数据请求逻辑
   │   │   ├── useSchema.js // schema逻辑
   │   │   ├── useVisitor.js // Python AST解析
   │   │   ├── useWS.js // socket.io的二次封装
   │   │   └── useX6.js // x6的一些逻辑封装
   ├── design-core
   │   ├── authentication.html
   │   ├── src
   │   │   ├── App.vue
   │   │   └── authentication // 登陆页面
   │   │      └── src
   │   │      ├── App.vue
   │   │      ├── components
   │   │      │   ├── login.vue
   │   │      │   └── register.vue
   │   │      └── main.js
   ├── plugins
   │   ├── materials // 物料 (paddlepaddle的网络物料)
   │   │   └── src
   │   │      ├── Main.vue
   │   │      ├── layer
   │   │      │   └── main.vue
   │   │      └── networks
   │   │      └── main.vue
   │   └── schema // 传输给后端的schema的预览窗
   │   └── src
   │       └── Main.vue
   └── settings // 物料的Props设计页面
      ├── code
      │   └── src
      │      └── Main.vue
      └── props
      ├── index.js
      ├── package.json
      └── src
         ├── Main.vue
         └── components
         ├── Empty.vue
         ├── ParamAttr.vue
         ├── enums.vue
         ├── list.vue
└── property-setting.vue
```
### 后端
[参考](./dl-flow-backend/README.md)
### 为什么结束节点和开始节点必须只有一个
因为目前生成的是`Sequential`而不是`Layer`.
`Layer`的确更加的灵活。但是问题也非常的显而易见。
我们设计的是又向无环图, 又向表明 A->B 是正确的,但是 B->A 是不一定的。假设有一幅图
```
Start
|
v
Node-1
|
-----+-----
| | |
V V V
END1 END2 END3
```
那么不管如何遍历最终节点,其实都是正确的。但在神经网络中,不同网络的运算顺序会有不同的结果。比如`先池化后卷积`和`先卷积后池化`的运算结果是不同的。进入训练阶段,训练结果也可能不同。这主要是因为函数的组合在某些条件下是不可交换的,我们也使用数学语言证明了这一点。详细可以参考[函数组合的交换性讨论](./dl-flow-backend/proof/函数组合的可交换性讨论.pdf)
## 前端流程图
![](./public/sequenceDiagram.png)
## 源码阅读顺序
```
dl-flow-back >> README.md >> code-generate/README.md >> code-generate.gateway.ts >> code-generate.service.ts >> ast.service.ts
dl-flow-frontend >> X6Canvas >> useX6 >> useSchma >> export
```

View File

@ -1,129 +0,0 @@
<p align="center">
<a href="https://opentiny.design/tiny-engine" target="_blank" rel="noopener noreferrer">
<img alt="OpenTiny Logo" src="logo.svg" height="100" style="max-width:100%;">
</a>
</p>
<p align="center">TinyEngine低代码引擎使能开发者定制低代码平台支持在线实时构建低码平台支持二次开发或集成低码平台能力</p>
[English](README.md) | 简体中文
🌈 特性:
- 跨端跨框架前端组件
- 支持在线实时构建、支持二次开发或被集成
- 直接生成可部署的源码,运行时无需引擎支撑
- 允许接入第三方组件、允许定制扩展插件
- 支持高代码与低代码,混合开发部署应用
- 平台接入 AI 大模型能力,辅助开发者构建应用
## 文档
- 介绍https://opentiny.design/tiny-engine#/home
- 使用文档https://opentiny.design/tiny-engine#/help-center/index
- 演示应用https://opentiny.design/tiny-engine#/tiny-engine-editor
## 使用
### 环境准备
- 安装 Node.js 18+
- 安装 pnpm 9+
```sh
$ npm install -g pnpm
```
### 使用 cli 创建低代码平台
```sh
# 创建低代码平台
$ npx @opentiny/tiny-engine-cli@latest create-platform <name>
# 进入低代码平台
$ cd <name>
# 安装依赖
$ pnpm install
```
### 本地开发,启动本地 mock 服务器,使用本地 mock 服务器的 mock 数据
> 创建低代码平台后自带的 mock Server 仅包含简单的后端 mock 功能,如果需要体验完整的后端服务能力,请参考下文启动 java 服务端。
```sh
$ pnpm dev
```
### 本地开发,启动 Java 服务端
java 服务端代码仓库https://github.com/opentiny/tiny-engine-backend-java
启动 Java 服务端进行前后端联调:
[前后端联调文档](https://docs.opentiny.design/tiny-engine/dev/debugging-of-java-backend)
### 物料同步[方案](https://docs.opentiny.design/tiny-engine/dev/material-sync-solution)
```sh
$ pnpm splitMaterials
```
```sh
$ pnpm buildMaterials
```
浏览器打开:`http://localhost:8080/?type=app&id=1&tenant=1&pageid=1`
`url search`参数:
- `type=app` 应用类型
- `id=xxx` 应用 ID
- `tenant=xxx` 组织 ID
- `pageid=xxx` 页面 ID
## 构建
```sh
# 构建设计器
pnpm run build:alpha 或 build:prod
```
## 里程碑
```mermaid
gantt
dateFormat YYYY-MM-DD
axisFormat %Y-%m-%d
1.0.0-beta.x version : 2023-09-25, 2024-05-20
1.0.0-rc version(refactor version) : 2024-10-01
1.0.0 version : 2024-11-01
2.0.0 version : 2024-12-16
2.1.0 version : 2025-01-02
2.2.0 version : 2025-02-19
2.3.0 version : 2025-03-14
2.4.0 version : 2025-04-07
2.5.0 version : 2025-05-15
```
## 🤝 参与贡献
如果你对我们的开源项目感兴趣,欢迎加入我们!🎉
参与贡献之前请先阅读[贡献指南](CONTRIBUTING.zh-CN.md)。
- 添加官方小助手微信 opentiny-official加入技术交流群
- 加入邮件列表 opentiny@googlegroups.com
## ❤️ 致谢
感谢所有为 TinyEngine 做出贡献的开发者们!
<p align="center">
<a href="https://github.com/opentiny/tiny-engine/graphs/contributors" target="_blank">
<img alt="贡献者" src="https://contrib.rocks/image?repo=opentiny/tiny-engine">
</a>
</p>
## 开源协议
[MIT](LICENSE)

View File

@ -1,11 +0,0 @@
export default {
id: 'engine.config',
theme: 'light',
material: ['./mock/bundle.json'],
scripts: [],
styles: [],
// 是否开启 TailWindCSS 特性
enableTailwindCSS: true,
// 是否开启 使用结构化CSS 特性
enableStructuredCss: false
}

View File

@ -1,12 +0,0 @@
# alpha mode, used by the "build:alpha" script
NODE_ENV=production
VITE_CDN_DOMAIN=https://registry.npmmirror.com
# 使用npmmirror的cdn 时,需要声明 VITE_CDN_TYPE=npmmirror
VITE_CDN_TYPE=npmmirror
# VITE_ORIGIN=
# 错误监控上报 url
VITE_ERROR_MONITOR_URL=/platform-center/api/platform/monitoring/event
# 是否开启错误监控
VITE_ERROR_MONITOR=false

View File

@ -1,8 +0,0 @@
# development mode, used by the "vite" command
NODE_ENV=development
VITE_CDN_DOMAIN=https://registry.npmmirror.com
# 使用npmmirror的cdn 时,需要声明 VITE_CDN_TYPE=npmmirror
VITE_CDN_TYPE=npmmirror
# request data via alpha service
# VITE_ORIGIN=

View File

@ -1,10 +0,0 @@
# CDN 本地化配置示例
# 将画布、页面预览需要的 vue、vue-i18n 等等依赖复制到构建产物中
VITE_LOCAL_IMPORT_MAPS=true
# 将本地物料 bundle.json 的 script 和 css 复制到构建产物中
VITE_LOCAL_BUNDLE_DEPS=true
# 将 VITE_LOCAL_BUNDLE_DEPS 复制到构建产物中的目录名称,默认为 local-cdn-static
VITE_LOCAL_IMPORT_PATH=local-cdn-static

View File

@ -1,8 +0,0 @@
# prod mode, used by the "build:prod" script
NODE_ENV=production
# VITE_CDN_DOMAIN=https://unpkg.com
VITE_CDN_DOMAIN=https://registry.npmmirror.com
# 使用npmmirror的cdn 时,需要声明 VITE_CDN_TYPE=npmmirror
VITE_CDN_TYPE=npmmirror
#VITE_ORIGIN=

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -1,35 +0,0 @@
{
"name": "designer-demo",
"private": true,
"version": "2.11.0",
"type": "module",
"scripts": {
"dev": "cross-env vite",
"dev:withAuth": "cross-env VITE_AUTH=true vite",
"build:alpha": "cross-env NODE_OPTIONS=--max-old-space-size=6144 vite build --mode alpha",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=10240 vite build",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@opentiny/tiny-engine": "workspace:^",
"@opentiny/tiny-engine-meta-register": "workspace:^",
"@opentiny/tiny-engine-utils": "workspace:*",
"@opentiny/vue": "~3.20.0",
"@opentiny/vue-design-smb": "~3.20.0",
"@opentiny/vue-icon": "~3.20.0",
"@opentiny/vue-locale": "~3.20.0",
"@opentiny/vue-renderless": "~3.20.0",
"@opentiny/vue-theme": "~3.20.0",
"@vueuse/core": "^9.6.0",
"vue": "^3.4.21"
},
"devDependencies": {
"@opentiny/tiny-engine-mock": "workspace:^",
"@opentiny/tiny-engine-vite-config": "workspace:^",
"@vitejs/plugin-vue": "^5.1.2",
"cross-env": "^7.0.3",
"vite": "^5.4.2",
"vitest": "3.0.9"
}
}

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Design Core Preview</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/preview.js"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 B

File diff suppressed because it is too large Load Diff

View File

@ -1,62 +0,0 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
// 注册表示例
import { META_SERVICE, META_APP } from '@opentiny/tiny-engine-meta-register'
import engineConfig from './engine.config'
import { HttpService } from './src/composable'
import scriptPlugin from './src/plugins/script'
export default {
[META_SERVICE.Http]: HttpService,
'engine.config': {
...engineConfig
},
// 配置 false 隐藏工具栏清空按钮
[META_APP.Clean]: false,
// 配置 false 隐藏大纲树,手动配置 tree-shaking 为 false仍然不会被 tree-shaking
// #__TINY_ENGINE_TREE_SHAKING__: false
[META_APP.OutlineTree]: false,
// 替换整个页面JS插件手动配置 tree-shaking 为 true
/* #__TINY_ENGINE_TREE_SHAKING__: true */
[META_APP.Page]: scriptPlugin,
// 新增模块
'engine.plugins.custom_id': {
...cuttomPlugin,
id: 'engine.plugins.custom_id'
},
// 调整插件顺序
[META_APP.Layout]: {
options: {
relativeLayoutConfig: {
[META_APP.Script]: {
insertBefore: META_APP.AppManage
},
// 调整插件顺序
[META_APP.Materials]: {
insertAfter: META_APP.State
},
// 调整插件上下位置
[META_APP.Schema]: {
insertBefore: META_APP.Materials
},
// 调整工具栏顺序
[META_APP.Save]: {
insertBefore: META_APP.ThemeSwitch
},
// 支持切换组
[META_APP.Lang]: {
insertAfter: META_APP.Breadcrumb
}
}
}
}
}

View File

@ -1,57 +0,0 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { META_SERVICE, META_APP } from '@opentiny/tiny-engine-meta-register'
import engineConfig from './engine.config'
import { HttpService } from './src/composable'
const baseURL = import.meta.env.BASE_URL || '.'
const baseURLWithoutSlash = baseURL.replace(/\/$/, '')
export default {
[META_SERVICE.Http]: HttpService,
'engine.config': {
...engineConfig
},
// 调整插件顺序示例:
[META_APP.Layout]: {
options: {
relativeLayoutConfig: {
[META_APP.Page]: {
insertBefore: META_APP.State
},
// 调整插件顺序
[META_APP.OutlineTree]: {
insertAfter: META_APP.Materials
},
// 调整插件上下位置
[META_APP.Schema]: {
insertBefore: META_APP.Help
},
// 调整工具栏顺序
[META_APP.Save]: {
insertAfter: META_APP.GenerateCode
},
// 支持切换组
[META_APP.Lang]: {
insertAfter: META_APP.ViewSetting
}
}
}
},
[META_APP.Preview]: {
options: {
// 配置预览跳转的 url根据实际业务需求进行配置
// 文档https://opentiny.design/tiny-engine#/help-center/course/dev/preview-api
previewUrl: ['prod', 'alpha'].includes(import.meta.env.MODE) ? `${baseURLWithoutSlash}/preview.html` : ''
}
}
}

View File

@ -1,136 +0,0 @@
import { createApp } from 'vue'
import { HttpService } from '@opentiny/tiny-engine'
import { useBroadcastChannel } from '@vueuse/core'
import { constants } from '@opentiny/tiny-engine-utils'
import Login from './Login.vue'
const LOGIN_EXPIRED_CODE = 401
const { BROADCAST_CHANNEL } = constants
const { post: globalNotify } = useBroadcastChannel({ name: BROADCAST_CHANNEL.Notify })
const procession = {
promiseLogin: null,
mePromise: {}
}
let loginVM = null
const showError = (url, message) => {
if (message === 'canceled') return // 取消请求场景不报错
globalNotify({
type: 'error',
title: '接口报错',
message: `报错接口: ${url} \n报错信息: ${message ?? ''}`
})
}
const preRequest = (config) => {
const isDevelopEnv = import.meta.env.MODE?.includes('dev')
if (isDevelopEnv && config.url.match(/\/generate\//)) {
config.baseURL = ''
}
const isVsCodeEnv = window.vscodeBridge
if (isVsCodeEnv) {
config.baseURL = ''
}
return config
}
const preResponse = (res) => {
if (res.data?.error) {
showError(res.config?.url, res?.data?.error?.message)
return Promise.reject(res.data.error)
}
return res.data?.data || res.data
}
const openLogin = () => {
if (!window.lowcode) {
const loginDom = document.createElement('div')
document.body.appendChild(loginDom)
loginVM = createApp(Login).mount(loginDom)
window.lowcode = {
platformCenter: {
Session: {
rebuiltCallback: function () {
loginVM.closeLogin()
procession.mePromise.resolve('login ok')
procession.promiseLogin = null
procession.mePromise = {}
}
}
}
}
}
return new Promise((resolve, reject) => {
if (!procession.promiseLogin) {
procession.promiseLogin = loginVM.openLogin(procession, '/api/rebuildSession')
procession.promiseLogin.then((response) => {
HttpService.apis.request(response.config).then(resolve, reject)
})
}
})
}
const errorResponse = (error) => {
// 用户信息失效时,弹窗提示登录
const { response } = error
if (response?.status === LOGIN_EXPIRED_CODE) {
// vscode 插件环境弹出输入框提示登录
if (window.vscodeBridge) {
return Promise.resolve(true)
}
// 浏览器环境弹出小窗登录
if (response?.headers['x-login-url']) {
return openLogin()
}
}
showError(error.config?.url, error?.message)
return response?.data.error ? Promise.reject(response.data.error) : Promise.reject(error.message)
}
const getConfig = (env = import.meta.env) => {
const baseURL = env.VITE_ORIGIN
// 仅在本地开发时,启用 withCredentials
const dev = env.MODE?.includes('dev')
// 获取租户 id
const getTenant = () => new URLSearchParams(location.search).get('tenant')
return {
baseURL,
withCredentials: dev,
headers: {
...(dev && { 'x-lowcode-mode': 'develop' }),
'x-lowcode-org': getTenant()
}
}
}
const customizeHttpService = () => {
const options = {
axiosConfig: getConfig(),
interceptors: {
request: [preRequest],
response: [[preResponse, errorResponse]]
}
}
HttpService.apis.setOptions(options)
return HttpService
}
export default customizeHttpService()

View File

@ -1 +0,0 @@
export { default as HttpService } from './http'

View File

@ -1,63 +0,0 @@
<template>
<span>我是自定义的 input configurator</span>
<tiny-input v-model="value" :type="type" :placeholder="placeholder" :rows="rows" @update:modelValue="change">
</tiny-input>
</template>
<script>
import { ref } from 'vue'
import { Input } from '@opentiny/vue'
export default {
name: 'MyInputConfigurator',
components: {
TinyInput: Input
},
props: {
modelValue: {
type: String
},
type: {
type: String
},
placeholder: {
type: String
},
suffixIcons: {
type: Array,
default: () => []
},
dataType: {
type: String
},
rows: {
type: Number,
default: 10
}
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const value = ref(props.modelValue)
const change = (val) => {
emit('update:modelValue', props.dataType === 'Array' ? val.split(',') : val)
}
return {
value,
change
}
}
}
</script>
<style lang="less" scoped>
.tiny-svg-size {
margin-left: 10px;
font-size: 16px;
&:hover {
cursor: pointer;
color: var(--te-common-text-primary);
}
}
</style>

View File

@ -1,5 +0,0 @@
import MyInputConfigurator from './MyInputConfigurator.vue'
export const configurators = {
MyInputConfigurator
}

View File

@ -1,27 +0,0 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { configurators } from './configurators/'
import 'virtual:svg-icons-register'
async function startApp() {
const registry = await import('../registry')
const { init } = await import('@opentiny/tiny-engine')
init({
// 合并多个注册表
registry: [registry.default],
configurators,
createAppSignal: ['global_service_init_finish']
})
}
startApp()

View File

@ -1,41 +0,0 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { defineEntry } from '@opentiny/tiny-engine-meta-register'
import engineConfig from '../engine.config'
import 'virtual:svg-icons-register'
async function startApp() {
const { initHook, HOOK_NAME, META_SERVICE, initPreview } = await import('@opentiny/tiny-engine')
const { HttpService } = await import('./composable')
const beforeAppCreate = () => {
initHook(HOOK_NAME.useEnv, import.meta.env)
}
const registry = {
[META_SERVICE.Http]: HttpService,
'engine.config': {
...engineConfig
}
}
defineEntry(registry)
initPreview({
registry,
lifeCycles: {
beforeAppCreate
}
})
}
startApp()

View File

@ -1,74 +0,0 @@
/**
* localCDN 功能测试
* 这个测试文件用于验证 localCDN 本地化功能是否正常工作
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { execSync } from 'node:child_process'
import { ensureEnvVarEnabled, backupEnvFile, restoreEnvFile } from './utils/envHelpers.js'
// 获取当前文件目录
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const projectRoot = path.resolve(__dirname, '..')
const distDir = path.resolve(projectRoot, 'dist')
const localCdnDir = path.resolve(distDir, 'local-cdn-static')
const envAlphaPath = path.resolve(projectRoot, 'env', '.env.alpha')
describe('localCDN 功能测试', () => {
beforeAll(() => {
// 备份环境变量文件
backupEnvFile(envAlphaPath)
// 确保环境变量正确设置
let envContent = fs.readFileSync(envAlphaPath, 'utf-8')
// 确保关键环境变量已启用
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_IMPORT_MAPS')
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_BUNDLE_DEPS')
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_IMPORT_PATH', 'local-cdn-static')
// 写回更新后的环境变量
fs.writeFileSync(envAlphaPath, envContent)
// 执行构建
execSync('pnpm run build:alpha', {
cwd: projectRoot,
stdio: 'inherit'
})
})
// 测试结束后恢复原始环境变量
afterAll(() => {
restoreEnvFile(envAlphaPath)
})
it('应该在构建后生成 local-cdn-static 目录', () => {
expect(fs.existsSync(localCdnDir)).toBe(true)
})
it('应该正确复制 @vue/devtools-api 依赖', () => {
// 寻找 @vue/devtools-api 文件夹
const devToolsDirs = fs.readdirSync(path.resolve(localCdnDir, '@vue'))
.find(dir => dir.startsWith('devtools-api@'))
expect(devToolsDirs).toBeDefined()
// 检查 index.js 是否存在
const indexJsExists = fs.existsSync(path.resolve(localCdnDir, '@vue', devToolsDirs, 'lib/esm/index.js'))
expect(indexJsExists).toBe(true)
})
it('应该正确复制 vue 依赖', () => {
// 寻找 vue 文件夹
const runtimeDirs = fs.readdirSync(localCdnDir).find(dir => dir.startsWith('vue@'))
expect(runtimeDirs).toBeDefined()
const vueProdDist = path.resolve(localCdnDir, runtimeDirs, 'dist/vue.runtime.esm-browser.js')
expect(fs.existsSync(vueProdDist)).toBe(true)
})
})

View File

@ -1,140 +0,0 @@
/**
* localCDN bundle依赖本地化测试
* 测试物料需要的CDN资源本地化功能
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { execSync } from 'node:child_process'
import { ensureEnvVarEnabled, updateCdnDomain, backupEnvFile, restoreEnvFile } from './utils/envHelpers.js'
// 获取当前文件目录
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const projectRoot = path.resolve(__dirname, '..')
const publicDir = path.resolve(projectRoot, 'public')
const bundleJsonDir = path.resolve(publicDir, 'mock')
const envAlphaPath = path.resolve(projectRoot, 'env', '.env.alpha')
const distDir = path.resolve(projectRoot, 'dist')
const bundleJsonPath = path.resolve(bundleJsonDir, 'bundle.json')
// 准备测试用的 bundle.json 文件
const testBundleJson = {
data: {
materials: {
packages: [
{
"name": "TinyVue组件库",
"package": "@opentiny/vue",
"version": "3.20.0",
"script": "https://unpkg.com/@opentiny/vue-runtime@3.20/dist3/tiny-vue-pc.mjs",
"css": "https://unpkg.com/@opentiny/vue-theme@3.20/index.css"
},
{
"name": "element-plus组件库",
"package": "element-plus",
"version": "2.4.2",
"script": "https://registry.npmmirror.com/element-plus/2.4.2/files/dist/index.full.mjs",
"css": "https://registry.npmmirror.com/element-plus/2.4.2/files/dist/index.css"
}
]
}
}
}
describe('localCDN bundle依赖本地化测试', () => {
let originalBundleJson = null
beforeAll(() => {
// 备份环境变量
backupEnvFile(envAlphaPath)
// 确保目录存在
if (!fs.existsSync(bundleJsonDir)) {
fs.mkdirSync(bundleJsonDir, { recursive: true })
}
// 备份原始的 bundle.json 文件(如果存在)
if (fs.existsSync(bundleJsonPath)) {
originalBundleJson = fs.readFileSync(bundleJsonPath, 'utf-8')
}
// 创建测试用的 bundle.json
fs.writeFileSync(bundleJsonPath, JSON.stringify(testBundleJson, null, 2))
// 设置环境变量
let envContent = fs.readFileSync(envAlphaPath, 'utf-8')
// 更新CDN域名
envContent = updateCdnDomain(envContent, 'https://unpkg.com')
// 确保启用了 bundle 依赖本地化
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_BUNDLE_DEPS')
fs.writeFileSync(envAlphaPath, envContent)
// 执行构建
execSync('pnpm run build:alpha', {
cwd: projectRoot,
stdio: 'inherit'
})
})
// 测试完成后清理测试文件并恢复环境变量
afterAll(() => {
// 恢复原始的 bundle.json 文件
if (originalBundleJson) {
fs.writeFileSync(bundleJsonPath, originalBundleJson)
} else if (fs.existsSync(bundleJsonPath)) {
// 如果原始文件不存在,则删除测试创建的文件
fs.unlinkSync(bundleJsonPath)
}
// 恢复环境变量
restoreEnvFile(envAlphaPath)
})
it('应该将物料CDN依赖本地化', () => {
const distBundleJsonPath = path.resolve(distDir, 'mock', 'bundle.json')
// 检查构建后的 bundle.json 是否存在
expect(fs.existsSync(distBundleJsonPath)).toBe(true)
// 检查构建后的 bundle.json 中是否已将远程URL替换为本地路径
const packages = JSON.parse(fs.readFileSync(distBundleJsonPath, 'utf-8')).data.materials.packages
// 检查 vue 的路径是否已本地化
expect(packages[0].script).not.toContain('https://unpkg.com')
expect(packages[0].script).toContain('./material-static/')
// 检查 element-plus 的路径是否已本地化
expect(packages[1].script).toContain('https://registry.npmmirror.com')
expect(packages[1].script).not.toContain('./material-static/')
expect(packages[1].css).toContain('https://registry.npmmirror.com')
expect(packages[1].css).not.toContain('./material-static/')
})
it('应该将物料依赖包复制到产物CDN目录', () => {
const localCdnDir = path.resolve(distDir, 'material-static/@opentiny')
// 检查 vue 是否已复制
const tinyVueDir = fs.readdirSync(localCdnDir)
.find(dir => dir.startsWith('vue-runtime@'))
const tinyVueThemeDir = fs.readdirSync(localCdnDir)
.find(dir => dir.startsWith('vue-theme@'))
expect(tinyVueDir).toBeDefined()
// 检查 tiny-vue-pc.mjs 是否存在
const tinyVueJsPath = path.resolve(localCdnDir, tinyVueDir, 'dist3', 'tiny-vue-pc.mjs')
const tinyVueCssPath = path.resolve(localCdnDir, tinyVueThemeDir, 'index.css')
expect(fs.existsSync(tinyVueJsPath)).toBe(true)
expect(fs.existsSync(tinyVueCssPath)).toBe(true)
// 检查 element-plus 是否已复制
const elementDir = fs.readdirSync(localCdnDir)
.find(dir => dir.startsWith('element-plus@'))
expect(elementDir).not.toBeDefined()
})
})

View File

@ -1,159 +0,0 @@
/**
* localCDN 自定义配置测试
* 测试文档中描述的自定义配置功能
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { execSync } from 'node:child_process'
import { ensureEnvVarEnabled, backupEnvFile, restoreEnvFile } from './utils/envHelpers.js'
// 获取当前文件目录
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const projectRoot = path.resolve(__dirname, '..')
const viteConfigPath = path.resolve(projectRoot, 'vite.config.js')
const envAlphaPath = path.resolve(projectRoot, 'env', '.env.alpha')
const originalViteConfig = fs.readFileSync(viteConfigPath, 'utf-8')
const distDir = path.resolve(projectRoot, 'dist')
/**
* 更新 registry.js 文件以支持自定义 importMap
*/
function updateRegistryFile() {
const registryPath = path.resolve(projectRoot, 'registry.js')
if (!fs.existsSync(registryPath)) {
return
}
// 备份原始文件
fs.copyFileSync(registryPath, registryPath + '.bak')
const registryContent = fs.readFileSync(registryPath, 'utf-8')
// 向 config 对象添加 importMap
const updatedContent = registryContent.replace(
/config: {([^}]*)}/,
`config: {$1,
importMap: {
imports: {
'vue': "\${VITE_CDN_DOMAIN}/vue\${versionDelimiter}3.4.21\${fileDelimiter}/dist/vue.runtime.esm-browser.js"
}
}
}`
)
fs.writeFileSync(registryPath, updatedContent)
}
/**
* 恢复 registry.js 文件
*/
function restoreRegistryFile() {
const registryPath = path.resolve(projectRoot, 'registry.js')
const backupPath = registryPath + '.bak'
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, registryPath)
fs.unlinkSync(backupPath)
}
}
describe('localCDN 自定义配置测试', () => {
beforeAll(() => {
// 备份原始的 vite.config.js
fs.writeFileSync(viteConfigPath + '.bak', originalViteConfig)
// 备份环境变量文件
backupEnvFile(envAlphaPath)
// 修改 vite.config.js 添加自定义配置
const updatedViteConfig = originalViteConfig.replace(
'const baseConfig = useTinyEngineBaseConfig({',
`const baseConfig = useTinyEngineBaseConfig({
importMapLocalConfig: {
importMap: {
imports: {
'vue': "\${VITE_CDN_DOMAIN}/vue\${versionDelimiter}3.4.21\${fileDelimiter}/dist/vue.runtime.esm-browser.prod.js"
}
},
copy: {
'vue': {
filePathInPackage: '/dist/'
}
}
},`
)
fs.writeFileSync(viteConfigPath, updatedViteConfig)
// 确保环境变量设置
let envContent = fs.readFileSync(envAlphaPath, 'utf-8')
// 确保关键环境变量已启用
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_IMPORT_MAPS')
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_BUNDLE_DEPS')
envContent = ensureEnvVarEnabled(envContent, 'VITE_LOCAL_IMPORT_PATH', 'local-cdn-static')
// 写回更新后的环境变量
fs.writeFileSync(envAlphaPath, envContent)
// 修改 registry.js 以支持自定义 importMap
updateRegistryFile()
// 执行构建
execSync('pnpm run build:alpha', {
cwd: projectRoot,
stdio: 'inherit'
})
})
// 测试完成后恢复原始配置
afterAll(() => {
// 恢复 vite.config.js
if (fs.existsSync(viteConfigPath + '.bak')) {
fs.copyFileSync(viteConfigPath + '.bak', viteConfigPath)
fs.unlinkSync(viteConfigPath + '.bak')
}
// 恢复环境变量
restoreEnvFile(envAlphaPath)
// 恢复 registry.js
restoreRegistryFile()
})
it('应该正确应用自定义 importMap 配置', () => {
const localCdnDir = path.resolve(distDir, 'local-cdn-static')
// 检查 vue 是否被正确复制
const vueDirs = fs.readdirSync(localCdnDir)
.filter(dir => dir.startsWith('vue@'))
.map(dir => path.resolve(localCdnDir, dir))
expect(vueDirs.length).toBeGreaterThan(0)
// 检查 dist 目录是否存在
const distExists = vueDirs.some(dir => {
return fs.existsSync(path.resolve(dir, 'dist'))
})
expect(distExists).toBe(true)
// 检查 vue.global.prod.js 文件是否存在
const vueJsExists = vueDirs.some(dir => {
return fs.existsSync(path.resolve(dir, 'dist', 'vue.runtime.esm-browser.js'))
})
expect(vueJsExists).toBe(true)
// 检查 dist 目录下的文件数量是否大于1 (因为我们的复制配置是复制整个文件夹)
const distFileCount = vueDirs.some(dir => {
const distPath = path.resolve(dir, 'dist')
return fs.existsSync(distPath) && fs.readdirSync(distPath).length > 1
})
expect(distFileCount).toBe(true)
})
})

View File

@ -1,69 +0,0 @@
/**
* 环境变量处理工具函数
*/
import fs from 'node:fs'
/**
* 更新环境变量如果变量不存在或值为false则设置为true
* @param {string} content - 环境变量文件内容
* @param {string} key - 环境变量名
* @returns {string} - 更新后的内容
*/
export function ensureEnvVarEnabled(content, key, value = 'true') {
// 检查是否包含该环境变量
const regex = new RegExp(`${key}\\s*=\\s*(.*)`, 'm')
const match = content.match(regex)
if (!match) {
// 变量不存在,添加
return `${content}\n${key}=${value}`
} else if (match[1].trim() !== value) {
// 变量存在但值为跟 value 不相等,替换为提供的值 value
return content.replace(regex, `${key}=${value}`)
}
// 变量已存在且不是false保持不变
return content
}
/**
* 更新环境变量中的CDN域名
* @param {string} content - 环境变量文件内容
* @param {string} cdnDomain - CDN域名
* @returns {string} - 更新后的内容
*/
export function updateCdnDomain(content, cdnDomain) {
const regex = /VITE_CDN_DOMAIN\s*=\s*(.*)/m
const match = content.match(regex)
if (!match) {
return `${content}\nVITE_CDN_DOMAIN=${cdnDomain}`
} else {
return content.replace(regex, `VITE_CDN_DOMAIN=${cdnDomain}`)
}
}
/**
* 备份环境变量文件
* @param {string} envFilePath - 环境变量文件路径
* @returns {void}
*/
export function backupEnvFile(envFilePath) {
if (fs.existsSync(envFilePath)) {
const backupPath = envFilePath + '.bak'
fs.copyFileSync(envFilePath, backupPath)
}
}
/**
* 恢复环境变量文件
* @param {string} envFilePath - 环境变量文件路径
* @returns {void}
*/
export function restoreEnvFile(envFilePath) {
const backupPath = envFilePath + '.bak'
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, envFilePath)
fs.unlinkSync(backupPath)
}
}

View File

@ -1,24 +0,0 @@
import path from 'node:path'
import { defineConfig, mergeConfig } from 'vite'
import { useTinyEngineBaseConfig } from '@opentiny/tiny-engine-vite-config'
export default defineConfig((configEnv) => {
const baseConfig = useTinyEngineBaseConfig({
viteConfigEnv: configEnv,
root: __dirname,
iconDirs: [path.resolve(__dirname, './node_modules/@opentiny/tiny-engine/assets/')],
useSourceAlias: true,
envDir: './env',
registryPath: './registry.js'
})
const customConfig = {
envDir: './env',
publicDir: path.resolve(__dirname, './public'),
server: {
port: 8090
}
}
return mergeConfig(baseConfig, customConfig)
})

View File

@ -1,19 +0,0 @@
import { defineConfig } from 'vitest/config'
import path from 'node:path'
export default defineConfig({
test: {
globals: true,
environment: 'node',
testTimeout: 1_000 * 60 * 10, // 10分钟超时因为构建可能需要较长时间
include: ['tests/**/*.test.js'],
hookTimeout: 1_000 * 60 * 10, // 10分钟超时因为构建可能需要较长时间
// 这里需要串行执行,否则构建会相互覆盖,无法测试
fileParallelism: false
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})

View File

@ -0,0 +1,4 @@
.env
node_modules
*.md
proof

View File

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

7
dl-flow-backend/.env Normal file
View File

@ -0,0 +1,7 @@
DB_URL=mongodb://localhost:27017:/dl-flow
JWT_EXPIRE_IN="1d"
JWT_SIGN_ALGORITHM="RS256"
JWT_PUB_KEY="./keys/pub.key"
JWT_PRI_KEY="./keys/pri.key"
PWD_SALT=”salt“
PWD_SALT_LEN=2

View File

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

42
dl-flow-backend/.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# do not upload public fold
public
# do not upload keys
keys
# do not upload data
data

View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

88
dl-flow-backend/README.md Normal file
View File

@ -0,0 +1,88 @@
# dl-flow backend
## 目录结构
```
├── README.md
├── docker-compose.yaml // 快速启动 compose示例
├── dockerfile // docker构建文件
├── global.d.ts
├── libs
│   ├── database // 数据库模块
│   ├── redis // 缓存模块
│   └── shared // 共用文件
├── src
│   ├── app.module.ts // 主文件
│   ├── auth-guard // token校验门禁 (可以理解为中间件, 不过nest的分类更加细致)
│   ├── code-generate
│   │   ├── ast
│   │   ├── ast.service.ts // ast生成服务
│   │   ├── code-generate.controller.ts // code-generate路由
│   │   ├── code-generate.gateway.ts // code-generate的websocket路由, 不过在nest中叫做gateway
│   │   ├── code-generate.module.ts // code-generate的module
│   │   ├── code-generate.schema.ts // code-generate的schema
│   │   └── code-generate.service.ts // code-generate的服务
│   ├── layer // layer获取与存储的http接口 (没什么技术含量全是CRUD)
│   ├── main.ts
│   ├── material // 物料的获取 (没什么技术含量全是CRUD)
│   ├── user // 用户的登陆注册
│   └── ws-exception // websocket的错误捕获, 主要用于捕获Exception和Runtime Error
├── tsconfig.build.json
├── tsconfig.json
└── webpack.config.js // 打包的配置文件, 主要是定义全局变量
```
### global.d.ts
```typescript
declare global {
namespace NodeJS {
// 为了让process.env.xxx的时候有类型提示
interface ProcessEnv {
DB_URL: string;
REDIS_HOST: string;
REDIS_PORT: number;
REDIS_DB: number;
REDIS_PASSWORD: string;
JWT_EXPIRE_IN: string;
JWT_SIGN_ALGORITHM: JwtSignOptions['algorithm'];
JWT_PUB_KEY: string;
JWT_PRI_KEY: string;
/**
* @deprecated
*/
PWD_SALT: string; // used for bcrypt
PWD_SALT_LEN: string;
}
}
// 开发环境标志
declare const __DEV__: boolean;
// 测试环境标志
declare const __TEST__: boolean;
}
export {};
```
## QA
### 为什么使用的是全局变量而不是环境变量?
`__DEV__`与`__TEST__`主要是用于测试环境与开发环境。如果一段代码只是测试环境需要(例如准备内存数据库),那么我们可以这么写
```typescript
if (__TEST__){
// do sth
}
```
之后我们只需要在jest的配置文件里的`global`配置项中中,将`__TEST__`定义为`true`就可以了。详细参考packages.json L94
而在打包时侯,`webpack`会将`__DEV__`与`__TEST__`占位符替换为false, 这样一来所有的测试代码与开发调试代码都将会被标记为`dead code`. 最后会被`webpack`自动剔除 (详细参考 webpack.config.js L12-)
### 为什么使用内存数据库而不是MOCK
mock一组数据是人来mock, 工作量是其次,最主要的问题是难以`靠近实战`. 之所以选择使用内存数据库而不是手动mock, 我给出如下原因
1. 不需要手动mock数据避免因为人脑无法达到**完全理性**而造成的**数据结构**问题
2. 内存数据库的可维护性很高
3. 数据的销毁简单,不需要本地启动复杂的环境

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
1

View File

@ -0,0 +1,31 @@
version: '3'
services:
mongodb:
image: mongo
ports:
- 27018:27017
redis:
image: redis
ports:
- 6379:6379
server:
image: gaonengwww/dl-flow-backend
ports:
- 9000:9000
environment:
- DB_URL=mongodb://mongodb:27017/dl-flow # 数据库地址
- REDIS_HOST=redis # redis地址 (必填)
- REDIS_PORT=6379 # redis端口 (必填)
- REDIS_DB=0 # redis数据库 (必填)
- REDIS_PASSWORD="" # redis密码
- JWT_EXPIRE_IN="1d" # JWT 过期时间 (必填)
- JWT_SIGN_ALGORITHM="RS256" # JWT签名算法, 要与密钥对符合, 例如密钥对是RSA 2048bit, 那么此处应该是 RS256 (必填)
- JWT_PUB_KEY=./keys/key.pub # JWT 公钥 (必填)
- JWT_PRI_KEY=./keys/key.pri # JWT 私钥 (必填)
- PWD_SALT=salt # bcrypt 盐(必填)
- PWD_SALT_LEN=12 # bcrypt 盐(必填)
# volumes: # 强烈将下述卷挂载到本地, 以避免数据丢失
# - ./_test/public:/public # 代码生成暂存位置
# - ./_test/keys:/keys # 密钥对存放位置
# - ./_test/data:/data # bundle.json与install.lock 存放位置

View File

@ -0,0 +1,25 @@
FROM node:18 as builder
WORKDIR /CODE
ADD . /CODE/
RUN npm install pnpm -g && pnpm install && pnpm build
FROM node:18-alpine as runner
COPY --from=builder /CODE/dist ./dist
COPY --from=builder /CODE/node_modules ./node_modules
VOLUME [ "/public", "/keys", "/data", "/examples"]
ENV DB_URL=""
ENV REDIS_HOST=""
ENV REDIS_PORT=""
ENV REDIS_DB=""
ENV REDIS_PASSWORD=""
ENV JWT_EXPIRE_IN=""
ENV JWT_SIGN_ALGORITHM=""
ENV JWT_PUB_KEY=""
ENV JWT_PRI_KEY=""
ENV PWD_SALT=""
ENV PWD_SALT_LEN=""
EXPOSE 9000-9900
CMD [ "node","dist/main.js" ]

File diff suppressed because it is too large Load Diff

26
dl-flow-backend/global.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
import { JwtSignOptions } from '@nestjs/jwt';
declare global {
namespace NodeJS {
interface ProcessEnv {
DB_URL: string;
REDIS_HOST: string;
REDIS_PORT: number;
REDIS_DB: number;
REDIS_PASSWORD: string;
JWT_EXPIRE_IN: string;
JWT_SIGN_ALGORITHM: JwtSignOptions['algorithm'];
JWT_PUB_KEY: string;
JWT_PRI_KEY: string;
/**
* @deprecated
*/
PWD_SALT: string; // used for bcrypt
PWD_SALT_LEN: string;
}
}
declare const __DEV__: boolean;
declare const __TEST__: boolean;
}
export {};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DatabaseService } from '../database.service';
describe('DatabaseService', () => {
let service: DatabaseService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DatabaseService],
}).compile();
service = module.get<DatabaseService>(DatabaseService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,30 @@
import { Logger, Module } from '@nestjs/common';
import { DatabaseService } from './database.service';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
imports: [
MongooseModule.forRootAsync({
useFactory: async () => {
let uri = process.env.DB_URL;
// 当且仅当为开发环境或测试环境时候,才会去启用内存数据库
if (__DEV__ || __TEST__) {
const { MongoMemoryReplSet } = await import('mongodb-memory-server');
const mongod = await MongoMemoryReplSet.create({
replSet: {
count: 2,
},
});
uri = mongod.getUri();
Logger.log(`Memory server url is: ${uri}`, 'DbModule');
}
return {
uri,
};
},
}),
],
})
export class DbModule {}

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class DatabaseService {}

View File

@ -0,0 +1,2 @@
export * from './database.module';
export * from './database.service';

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "../../dist/libs/database"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

View File

@ -0,0 +1,2 @@
export * from './redis.module';
export * from './redis.service';

View File

@ -0,0 +1,50 @@
import { Module } from '@nestjs/common';
import { RedisService } from './redis.service';
import {
RedisModule as Redis,
RedisModuleOptions,
} from '@liaoliaots/nestjs-redis';
export const memoryRedis = async () => {
if (__DEV__ || __TEST__) {
const { RedisMemoryServer } = await import('redis-memory-server');
const server = new RedisMemoryServer({
instance: {
ip: '127.0.0.1',
port: 6379,
},
});
await server.start();
return server;
}
};
@Module({
providers: [RedisService],
exports: [RedisService],
imports: [
Redis.forRootAsync({
useFactory: async (): Promise<RedisModuleOptions> => {
if (__DEV__ || __TEST__) {
const server = await memoryRedis();
return {
config: {
host: await server.getIp(),
port: await server.getPort(),
},
};
}
return {
readyLog: true,
config: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
db: process.env.REDIS_DB,
password: process.env.REDIS_PASSWORD,
},
};
},
}),
],
})
export class RedisModule {}

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class RedisService {}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "../../dist/libs/redis"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

View File

@ -0,0 +1,74 @@
import { ValidationOptions, registerDecorator } from 'class-validator';
export type Enum = {
id: string;
label: string;
value: string;
default?: boolean;
};
export type Label = {
zh_CN: string;
en_US: string;
};
export type Property = {
id: string;
label: Label;
type: string;
default: string | number | null | boolean;
enums: Enum[];
data: any;
};
export const isProperty = (object: unknown | unknown[]) => {
if (Array.isArray(object)) {
return false;
}
if (object instanceof Object) {
const maybeProperty: Partial<Property> = object;
const keys = ['id', 'label', 'type', 'default'];
const maybePropertyKeys = Object.keys(maybeProperty);
if (!maybePropertyKeys.length) {
return false;
}
return maybePropertyKeys.every((key) => keys.includes(key));
}
return false;
};
export function IsProperty(validationOptions?: ValidationOptions) {
return function (object: Partial<Property>, propertyName: string) {
registerDecorator({
name: 'isProperty',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any) {
return isProperty(value);
},
},
});
};
}
export function IsProperties(validationOptions?: ValidationOptions) {
return function (object: Partial<Property>, propertyName: string) {
registerDecorator({
name: 'IsProperties',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: unknown[]) {
console.log(value);
if (!value?.length) {
return false;
}
return value.every((v) => isProperty(v));
},
},
});
};
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "../../dist/libs/shared"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

View File

@ -0,0 +1,39 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"webpack": true,
"webpackConfigPath": "webpack.config.js"
},
"projects": {
"database": {
"type": "library",
"root": "libs/database",
"entryFile": "index",
"sourceRoot": "libs/database/src",
"compilerOptions": {
"tsConfigPath": "libs/database/tsconfig.lib.json"
}
},
"shared": {
"type": "library",
"root": "libs/shared",
"entryFile": "index",
"sourceRoot": "libs/shared/src",
"compilerOptions": {
"tsConfigPath": "libs/shared/tsconfig.lib.json"
}
},
"redis": {
"type": "library",
"root": "libs/redis",
"entryFile": "index",
"sourceRoot": "libs/redis/src",
"compilerOptions": {
"tsConfigPath": "libs/redis/tsconfig.lib.json"
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,109 @@
{
"name": "dl-flow-backend",
"version": "0.0.7",
"description": "",
"author": "",
"private": true,
"license": "MIT",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"",
"start": "cross-env DB_URL=mongodb://127.0.0.1:27017/dl-flow?directConnection=true nest start --watch",
"start:dev": "cross-env NODE_ENV=DEV DB_URL=mongodb://127.0.0.1:27017/?directConnection=true nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest --clear-cache && jest --force-exit --detectOpenHandles",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/jest/bin/jest.js --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@liaoliaots/nestjs-redis": "^9.0.5",
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "*",
"@nestjs/mongoose": "^10.0.2",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/platform-socket.io": "^10.3.2",
"@nestjs/websockets": "^10.3.2",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"compressing": "^1.10.0",
"ioredis": "^5.3.2",
"mongoose": "^8.1.1",
"ms": "^2.1.3",
"ramda": "^0.29.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0",
"socket.io": "^4.7.4"
},
"devDependencies": {
"@antv/x6": "^2.18.1",
"@golevelup/ts-jest": "^0.4.0",
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.21",
"@types/jest": "29.5.1",
"@types/ms": "^0.7.34",
"@types/node": "18.16.12",
"@types/ramda": "^0.29.10",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"express": "^4.18.2",
"jest": "29.5.0",
"mongodb-memory-server": "^9.1.6",
"prettier": "^2.3.2",
"redis-memory-server": "^0.10.0",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "29.1.0",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.2.0",
"typescript": "^5.0.0",
"webpack": "^5.90.1"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": ".",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "./coverage",
"globals": {
"__TEST__": true,
"__DEV__": false
},
"roots": [
"<rootDir>/src/",
"<rootDir>/libs/"
],
"moduleNameMapper": {
"^@app/database(|/.*)$": "<rootDir>/libs/database/src/$1",
"^@app/shared(|/.*)$": "<rootDir>/libs/shared/src/$1",
"^@app/redis(|/.*)$": "<rootDir>/libs/redis/src/$1"
},
"verbose": true,
"maxWorkers": 1
}
}

View File

@ -0,0 +1,170 @@
import { Logger, Module, OnModuleInit } from '@nestjs/common';
import { LayerModule } from './layer/layer.module';
import { DbModule } from '@app/database';
import { MaterialModule } from './material/material.module';
import { CodeGenerateModule } from './code-generate/code-generate.module';
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
writeFileSync,
} from 'fs';
import { basename, join } from 'path';
import { UserModule } from './user/user.module';
import { JwtModule } from '@nestjs/jwt';
import { InjectModel, MongooseModule } from '@nestjs/mongoose';
import { Material, MaterialSchema } from './material/material.schema';
import { Model } from 'mongoose';
import { RedisModule } from '@app/redis';
import { InjectRedis } from '@liaoliaots/nestjs-redis';
import { Redis } from 'ioredis';
import { ProjectModule } from './project/project.module';
import { User, UserSchema } from './user/user.schema';
import { Project, ProjectSchema } from './project/entities/project.entity';
import { isEmpty } from 'ramda';
import { UserService } from './user/user.service';
import { ProjectService } from './project/project.service';
@Module({
imports: [
DbModule,
LayerModule,
MaterialModule,
CodeGenerateModule,
UserModule,
JwtModule.register({
global: true,
publicKey: readFileSync(
join(process.cwd(), process.env.JWT_PUB_KEY ?? './keys/pub.key'),
),
privateKey: readFileSync(
join(process.cwd(), process.env.JWT_PRI_KEY ?? './keys/pri.key'),
),
signOptions: {
algorithm: process.env.JWT_SIGN_ALGORITHM ?? 'RS256',
expiresIn: process.env.JWT_EXPIRE_IN ?? '1 day',
},
}),
MongooseModule.forFeature([
{
name: Material.name,
schema: MaterialSchema,
},
{
name: User.name,
schema: UserSchema,
},
{
name: Project.name,
schema: ProjectSchema,
},
]),
RedisModule,
ProjectModule,
],
providers: [UserService, ProjectService],
})
export class AppModule implements OnModuleInit {
private readonly Logger: Logger = new Logger('App');
constructor(
@InjectRedis()
private readonly redis: Redis,
@InjectModel(Material.name)
private readonly MaterialModel: Model<Material>,
@InjectModel(User.name)
private readonly UserModel: Model<User>,
@InjectModel(Project.name)
private readonly ProjectModel: Model<Project>,
private readonly userService: UserService,
private readonly projectService: ProjectService,
) {}
async onModuleInit() {
const root = process.cwd();
const publicPath = join(root, 'public');
const lock = join(root, 'data', 'install.lock');
const bundle = join(root, 'data', 'bundle.json');
const examplePath = join(root, 'examples');
if (existsSync(lock) && !__DEV__) {
this.Logger.log('Lock file exists');
return;
}
if (!existsSync(bundle)) {
this.Logger.warn('bundle.json not exists');
}
if (existsSync(bundle)) {
this.Logger.log('bundle.json exists');
const { data } = JSON.parse(readFileSync(bundle).toString());
const types = data.types;
const materials = data.materials;
if (types) {
this.Logger.log('Insert types');
try {
for (const [key, value] of Object.entries(types)) {
await this.redis.hset('types', { [key]: JSON.stringify(value) });
}
} catch (err) {
const e = err as Error;
this.Logger.error('Insert types fail', e.stack);
process.exit(-1);
}
}
if (materials) {
this.Logger.log('Insert materials');
try {
await this.MaterialModel.insertMany(materials);
} catch (err) {
const e = err as Error;
this.Logger.error('Insert Materials fail', e.stack);
process.exit(-1);
}
}
}
if (!existsSync(publicPath)) {
mkdirSync(publicPath);
}
const adminUser = await this.UserModel.findOne({
email: 'admin@no-reply.com',
});
let profile;
let token;
if (!adminUser) {
this.Logger.warn('Not find admin user');
profile = await this.userService.register({
email: 'admin@no-reply.com',
nick: 'Admin',
password: 'admin',
});
token = (
await this.userService.login({
email: 'admin@no-reply.com',
password: 'admin',
})
).jwt;
this.Logger.log('Create Admin user success');
}
if (existsSync(examplePath)) {
const examples = readdirSync(examplePath).map((fileName) => {
return [basename(fileName, '.json'), join(examplePath, fileName)];
});
for (const [exampleName, examplePath] of examples) {
this.Logger.log(`Insert ${exampleName} example`);
const content = JSON.parse(readFileSync(examplePath).toString());
if (!isEmpty(content['data']) && !isEmpty(content['graphData'])) {
const { projectId } = await this.projectService.create(
{ name: exampleName },
token,
);
await this.projectService.updateProject(projectId, {
...content,
});
this.Logger.log(`Insert ${exampleName} success`);
} else {
this.Logger.warn(`Example should contain data and graphdata`);
}
}
}
writeFileSync(lock, '1');
}
}

View File

@ -0,0 +1,37 @@
import { InjectRedis } from '@liaoliaots/nestjs-redis';
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { Redis } from 'ioredis';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private jwt: JwtService,
@InjectRedis()
private redis: Redis,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(req);
try {
this.jwt.verify(token);
} catch {
throw new UnauthorizedException();
}
// 如果redis中不存在token, 那么也提示未登陆错误
if (!Boolean(await this.redis.exists(token))) {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

View File

@ -0,0 +1,9 @@
# code generate
## 流程图
![](../../../public/3bb36ea8-b6cc-48ae-b227-1118f3b0a4c6.png)
## 附件
[函数组合的可交换性讨论](../../proof/函数组合的可交换性讨论.pdf)

View File

@ -0,0 +1,409 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AST, VarDecl } from '../ast.service';
import { Material } from '../../material/material.schema';
import { Cell, Edge } from '@antv/x6';
import { Layer } from '../code-generate.schema';
import { StandardizationNodes } from '../code-generate.service';
describe('AST', () => {
let service: AST;
const layer = {
code: `
class Layer:
def __init__(self,x):
pass
`,
clazz: 'Layer',
id: 'layer-1',
label: {
zh_CN: '',
en_US: '',
},
properties: [],
del: false,
mode: 'layer',
};
const buildNode = (total: number, startId = 0) => {
const arr: {
id: string;
shape: string;
position: {
x: number;
y: number;
};
size: {
width: number;
height: number;
};
zIndex: number;
data: Material;
}[] = [];
for (let i = 1; i <= total; i++) {
const obj = {
id: `node${i + startId}`,
shape: 'dag-node',
position: { x: 0, y: 0 },
size: { width: 0, height: 0 },
attrs: {},
zIndex: 0,
data: {
mode: 'nn',
id: 'Conv1d',
properties: [],
} as any,
};
arr.push(obj);
}
return arr;
};
const buildLayer = (total: number): any[] => {
const arr = [];
for (let i = 1; i <= total; i++) {
arr.push({
id: `layer-${i}`,
shape: 'dag-node',
label: {
zh_CN: '',
en_US: '',
},
data: {
code: `
class Layer${i}:
def __init__(self,x):
pass
`,
clazz: `Layer${i}`,
properties: [{ id: 'x', data: 1 }],
del: false,
mode: 'layer',
},
});
}
return arr;
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AST],
}).compile();
service = module.get<AST>(AST);
});
it.skip('should be defined', () => {
expect(service).toBeDefined();
});
it('standardization', () => {
expect(service.standardization('123', 'string')).toBe(`'123'`);
expect(service.standardization('123', 'number')).toBe(`123`);
expect(service.standardization('[0,0]', 'array')).toBe('[0,0]');
expect(service.standardization('123', 'boolean')).toBe('true');
expect(service.standardization('true', 'boolean')).toBe('true');
expect(service.standardization('false', 'boolean')).toBe('false');
});
describe('buildNN', () => {
it('non properties', () => {
const node = buildNode(1)[0];
expect(service.buildNN(node.data, node.id).name).toBe(`node${node.id}`);
expect(service.buildNN(node.data, node.id).codeGen()).toBe(
`node${node.id} = paddle.nn.${node.data.id}()`,
);
});
it('has properties', () => {
const node = buildNode(1)[0];
node.data.properties.push(
{
id: 'in_channel',
data: 256,
label: {
zh_CN: '',
en_US: '',
},
type: 'number',
default: '',
enums: [],
},
{
id: 'out_channel',
data: 128,
label: {
zh_CN: '',
en_US: '',
},
type: 'number',
default: '',
enums: [],
},
{
id: 'weight_attr',
data: {
name: 'weight',
learning_rate: 1e-5,
},
label: {
zh_CN: '',
en_US: '',
},
type: 'ParamAttr',
default: '',
enums: [],
},
{
id: 'test',
type: 'list',
data: [1, '2', true, {}],
label: {
zh_CN: '',
en_US: '',
},
default: '',
enums: [],
},
);
expect(service.buildNN(node.data, node.id).name).toBe(`node${node.id}`);
expect(service.buildNN(node.data, node.id).codeGen()).toBe(
`node${node.id} = paddle.nn.${node.data.id}(in_channel = 256,out_channel = 128,weight_attr = ParamAttr(name = 'weight',learning_rate = 0.00001),test = [1,'2',true,{}])`,
);
});
});
describe('group', () => {
it('non nest', () => {
const group = {
id: 'group-1',
shape: 'group-node',
position: {
x: 0,
y: 0,
},
size: {
width: 0,
height: 0,
},
attrs: undefined,
zIndex: 0,
data: new Material(),
children: ['node-1', 'node-2'],
};
const ast = service.buildGroup(group as any, {
'group-1': group as any,
'node-1': {
id: 'node-1',
shape: 'dag-node',
data: {
id: 'Conv1D',
mode: 'nn',
properties: [],
} as Material,
position: { x: 0, y: 0 },
size: {
width: 0,
height: 0,
},
attrs: undefined,
zIndex: 0,
},
'node-2': {
id: 'node-2',
shape: 'dag-node',
data: {
id: 'Conv1D',
mode: 'nn',
properties: [],
} as Material,
position: { x: 0, y: 0 },
size: {
width: 0,
height: 0,
},
attrs: undefined,
zIndex: 0,
},
});
// expect(ast.children.length).toBe(3);
// expect(ast.children[0]).toBeInstanceOf(VarDecl);
// expect(ast.children[1]).toBeInstanceOf(VarDecl);
// expect(ast.children[2]).toBeInstanceOf(VarDecl);
expect(ast.children[0].codeGen()).toContain('group1');
});
it('has nest', () => {
const nodes = {
'node-4': buildNode(1, 3)[0],
'node-11': buildNode(1, 10)[0],
'group-3': {
id: 'group-3',
shape: 'group',
children: ['node-11'] as any,
data: new Material(),
position: { x: 0, y: 0 },
size: {
width: 0,
height: 0,
},
attrs: undefined,
zIndex: 0,
},
'group-2': {
id: 'group-2',
shape: 'group',
data: new Material(),
position: { x: 0, y: 0 },
size: {
width: 0,
height: 0,
},
attrs: undefined,
zIndex: 0,
children: ['node-4', 'group-3'] as any,
},
'node-2': {
id: 'node-2',
shape: 'dag-node',
data: {
id: 'Conv1D',
mode: 'nn',
properties: [],
} as Material,
position: { x: 0, y: 0 },
size: {
width: 0,
height: 0,
},
attrs: undefined,
zIndex: 0,
},
'node-1': {
id: 'node-1',
shape: 'dag-node',
data: {
id: 'Conv1D',
mode: 'nn',
properties: [],
} as Material,
position: { x: 0, y: 0 },
size: {
width: 0,
height: 0,
},
attrs: undefined,
zIndex: 0,
},
};
const group = {
id: 'group-1',
shape: 'group-node',
position: {
x: 0,
y: 0,
},
size: {
width: 0,
height: 0,
},
attrs: undefined,
zIndex: 0,
data: new Material(),
children: ['node-1', 'node-2', 'group-2'],
};
const ast = service.buildGroup(group as any, nodes as any);
expect(ast.codeGen()).toBe(`group_group3 = paddle.concat(x=[nodenode11])
group_group2 = paddle.concat(x=[group_group3,nodenode4])
group_group1 = paddle.concat(x=[group_group2,nodenode2,nodenode1])`);
});
});
it('build', () => {
const node: StandardizationNodes = {
'node-1': {
id: 'node-1',
shape: 'node',
position: {
x: 0,
y: 0,
},
size: { width: 0, height: 0 },
attrs: {},
zIndex: 0,
data: {
mode: 'nn',
id: 'Conv1D',
properties: [],
} as Material,
},
'node-2': {
id: 'node-2',
shape: 'node',
position: {
x: 0,
y: 0,
},
size: { width: 0, height: 0 },
attrs: {},
zIndex: 0,
data: {
mode: 'nn',
id: 'Conv1D',
properties: [],
} as Material,
},
layer: {
...(buildLayer(1)[0] as Layer),
} as any,
'node-3': {
id: 'node-3',
shape: 'node',
position: {
x: 0,
y: 0,
},
size: { width: 0, height: 0 },
attrs: {},
zIndex: 0,
data: {
mode: 'nn',
id: 'Conv1D',
properties: [],
} as Material,
},
'layer-2': {
...buildLayer(1)[0],
},
group: {
id: 'group',
shape: 'group',
position: {
x: 0,
y: 0,
},
size: { width: 0, height: 0 },
attrs: {},
zIndex: 0,
children: ['node-3', 'layer-2'] as any,
data: new Material(),
},
};
const ast = service.build(
[
node['node-1'],
node['node-2'],
node['layer'],
node['node-3'],
node['group'],
],
node,
);
expect(ast.codeGen().replace(/\n| /gim, '')).toEqual(
`true = True
false = False
nodenode1 = paddle.nn.Conv1D()
nodenode2 = paddle.nn.Conv1D()
class Layer1:
def __init__(self,x):
pass
layer1 = Layer1(x=1)
nodenode3 = paddle.nn.Conv1D()
group_group = paddle.concat(x=[layer1,nodenode3])
model=paddle.nn.Sequential(nodenode1,nodenode2,group_group)`.replace(
/\n| /gim,
'',
),
);
});
});

View File

@ -0,0 +1,46 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CodeGenerateService } from '../code-generate.service';
import { Cell, Edge } from '../code-generate.schema';
import { readFileSync } from 'fs';
const groupNestTestCase = JSON.parse(
readFileSync('./group-nest-test-case.json').toString(),
);
describe('code generate', () => {
let service: CodeGenerateService;
beforeEach(async () => {
const moudle: TestingModule = await Test.createTestingModule({
providers: [CodeGenerateService],
}).compile();
service = moudle.get<CodeGenerateService>(CodeGenerateService);
});
it('standardizationEdge', () => {
expect(
service.standardizationEdge(
groupNestTestCase.payload.edges as unknown as Edge[],
service.standardizationNode(groupNestTestCase.payload.cells),
),
).toBeDefined();
console.log(
service.standardizationEdge(
groupNestTestCase.payload.edges as unknown as Edge[],
service.standardizationNode(groupNestTestCase.payload.cells),
),
);
});
it('sequencingNode', () => {
const nodes = service.standardizationNode(
service.extract(
groupNestTestCase.payload.cells as unknown as Cell[],
(cell) => cell.shape.includes('node'),
),
);
const edges = service.standardizationEdge(
groupNestTestCase.payload.edges as unknown as Edge[],
service.standardizationNode(groupNestTestCase.payload.cells),
);
const { start, end } = groupNestTestCase.meta;
expect(service.sequencingNode(nodes, edges, start, end)).toBeDefined();
});
});

View File

@ -0,0 +1,386 @@
import { Injectable } from '@nestjs/common';
import { Material } from '../material/material.schema';
import { Layer } from '../layer/layer.schema';
import { Cell } from './code-generate.schema';
import { Exception, StandardizationNodes } from './code-generate.service';
import { randomUUID } from 'crypto';
@Injectable()
export class AST {
/**
* # Reference material
* @see {@link https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl03.html|Kaleidoscope}
* @see {@link https://github.com/xkbeyer/liquid|liquid}
*/
build(cells: Cell[], standardizationNodes: StandardizationNodes) {
const ast: IAST = {
type: 'root',
children: [
// I HATE PYTHON
new VarDecl('true', new Identifier('True')),
new VarDecl('false', new Identifier('False')),
],
codeGen: () => {
// 每个 AstItem 必须存在一个 codeGen 函数只要根节点调用第一层节点的codeGen
// 并且在开发时, 确保其他节点的codeGen函数中也遍历了子节点且调用codeGen就可以完成整个语法树的代码生成
// 不过考虑到说动频接换行符太繁琐, 这里包装成了数组, 最后join就可以
return ast.children.map((child) => child.codeGen()).join('\n');
},
};
for (const cell of cells) {
const data = cell.data as Material | Layer;
let item;
if (this.isGroup(cell)) {
item = this.buildGroup(cell, standardizationNodes);
ast.children.push(item);
continue;
}
if (this.isLayer(data)) {
item = this.buildLayer(data);
ast.children.push(item);
const callee = new Identifier(data.clazz);
const clazzInstance = new CallExpression(
callee,
data.properties.map((v) => `${v.id}=${v.data}`),
);
const instance = new VarDecl(cell.id.replace('-', ''), clazzInstance);
ast.children.push(instance);
continue;
}
if (this.isNN(data)) {
item = this.buildNN(data, cell.id);
ast.children.push(item);
continue;
}
}
ast.children.push(new VarDecl('model', this.buildSequential(ast)));
return ast;
}
/**
* Eg.
* abcd -> 'abcd'
*/
standardization(val: unknown, type?: string): string {
if (val === 'true' || val === 'false') {
return val;
}
if (type === 'list') {
return `[${(val as any[]).map((v) =>
typeof v !== 'string' ? JSON.stringify(v) : `'${v}'`,
)}]`;
}
if (type === 'boolean') {
return `${Boolean(val)}`;
}
if (type === 'string') {
return `'${val}'`;
}
return `${val}`;
}
standardizationParamAttr(key: string, val: string) {
switch (key) {
case 'name':
return this.standardization(
val === '' ? randomUUID().replace(/\-/gim, '') : val,
'string',
);
case 'learning_rate':
return this.standardization(val, 'number');
case 'trainable':
return this.standardization(val, 'boolean');
case 'do_model_average':
return this.standardization(val, 'boolean');
case 'need_clip':
return this.standardization(val, 'boolean');
default:
return val;
}
}
buildNN(nn: Material, cellId: string) {
/*
* arguments, string数组, new CallExpress或者 new VarDecl时就不需要再次递归调用`codeGen`
*/
const args = nn.properties
.map((v) => {
const id = v.id;
if (typeof v.data === 'object' && !Array.isArray(v.data)) {
const args: string[] = [];
for (const key of Object.keys(v.data)) {
if (v.data[key] !== undefined) {
const val =
v.type === 'ParamAttr'
? this.standardizationParamAttr(key, v.data[key])
: this.standardization(
v.data[key] ?? v.default ?? 'None',
v.type,
);
args.push(new VarDecl(key, new Identifier(val)).codeGen());
}
}
const callExpression = new CallExpression(
new Identifier(v.type),
args,
);
const varDecl = new VarDecl(id, callExpression);
return varDecl.codeGen();
}
if (v.data) {
return new VarDecl(
id,
// 标准化参数
// 比如字符串 abcd 会被标准化为 'abcd'
new Identifier(this.standardization(v.data, v.type)),
).codeGen();
}
})
.filter((v) => v !== undefined);
const callee = new Identifier(`paddle.nn.${nn.id}`);
const fnCall = new CallExpression(callee, args);
const varDecl = new VarDecl(`node${cellId.replace(/-/gim, '')}`, fnCall);
return varDecl;
}
extractGroup(group: Cell, nodeTable: StandardizationNodes) {
const stack: Cell[] = [];
if (this.isGroup(group)) {
stack.push(group);
}
for (const child of (group.children as unknown as string[]) ?? []) {
// 考虑group嵌套问题
if (this.isGroup(nodeTable[child])) {
stack.push(...this.extractGroup(nodeTable[child], nodeTable));
}
}
return stack;
}
isChild(group: Cell, child: Cell) {
return (group.children as unknown as string[]).includes(child.id);
}
/**
* @description
*
* Layer本质是一组代码, ClazzDef一下就好.
*/
buildLayer(layer: Layer) {
const clazzDef = new ClazzDef(layer.code);
return clazzDef;
}
buildGroup(group: Cell, standardizationNodes: StandardizationNodes) {
const ast = new GroupAst();
const stack: [Cell, 'start' | 'node' | 'end'][] = [];
const groups = this.extractGroup(group, standardizationNodes);
for (const g of groups) {
stack.push([g, 'start']);
for (const child of (g.children as unknown as string[]) ?? []) {
if (standardizationNodes[child]) {
stack.push([standardizationNodes[child], 'node']);
}
}
stack.push([g, 'end']);
}
let activeGroup: Cell | null = null;
/**
*
* node_1 = ...
* node_2 = ...
* group_1 = paddle.concat(node_1,node_2)
* node_3 = ...
* group_2 = paddle.concat(node_1,node_2,node_3, group_1)
* LC. 20
* @see https://leetcode.cn/problems/valid-parentheses/description/
*/
while (stack.length) {
const [cell, type] = stack.pop();
const children = [];
if (type === 'end') {
activeGroup = cell;
}
if (type === 'node') {
if (this.isGroup(cell)) {
children.push(`group_${cell.id.replace(/-/gim, '')}`);
} else {
if (this.isNN(cell.data)) {
children.push(`node${cell.id.replace(/-/gim, '')}`);
}
if (this.isLayer(cell.data)) {
children.push(`${cell.id.replace('-', '')}`);
}
}
while (true) {
const [cell, type] = stack.pop();
if (type === 'start') {
break;
}
if (this.isGroup(cell)) {
children.push(`group_${cell.id.replace(/-/gim, '')}`);
} else {
if (this.isNN(cell.data)) {
children.push(`node${cell.id.replace(/-/gim, '')}`);
}
if (this.isLayer(cell.data)) {
children.push(`${cell.id.replace('-', '')}`);
}
}
}
if (!activeGroup) {
throw new Exception('Schema错误, 请检查Schema格式');
}
const callee = new Identifier('paddle.concat');
/**
* @see https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/concat_cn.html
*/
const call = new CallExpression(callee, [
['x=[', children.join(','), ']'].join(''),
]);
const concatVar = new VarDecl(
`group_${activeGroup.id}`.replace(/-/gim, ''),
call,
);
ast.children.push(concatVar);
ast.childId.push(...children);
}
}
return ast;
}
buildSequential(ast: IAST) {
const stack = [
...ast.children
.map((child) => {
// 是变量直接拿到变量名
if (child instanceof VarDecl) {
return child.name;
}
// 这里不过是为了方便开发时区分, 将Group做了一个新的AST root, 被称作GroupAst
//
if (child instanceof GroupAst) {
return child.children.map((child) =>
child instanceof VarDecl ? child.name : null,
);
}
return null;
})
.flat()
.filter(
(child) => child !== null && child !== 'true' && child !== 'false',
),
];
const groups = ast.children
.map((child) => (child instanceof GroupAst ? child : null))
.filter((child) => child !== null);
const childrenId = groups
.map((group) => group.childId)
.reduce((pre, cur) => [...pre, ...cur], []);
return new CallExpression(
new Identifier('paddle.nn.Sequential'),
stack.filter((item) => !childrenId.includes(item)),
);
}
isGroup(cell: Cell) {
return cell?.shape && cell.shape.includes('group');
}
isNN(data: Material | Layer): data is Material {
return data.mode === 'nn';
}
isLayer(data: Material | Layer): data is Layer {
return data.mode === 'layer';
}
}
// base type
export class Node {}
export class VarDecl implements IVarDecl, Node {
public name: string;
public val: ASTItem;
constructor(name: string, val: ASTItem) {
this.name = name;
this.val = val;
}
codeGen() {
return `${this.name} = ${this.val.codeGen()}`.replace('\t', '');
}
}
export class Identifier implements IIdentifier, Node {
name: string;
constructor(name: string) {
this.name = name;
}
codeGen() {
return `${this.name}`;
}
}
export class CallExpression implements ICallExpression, Node {
callee: IIdentifier;
args: any[];
constructor(callee: IIdentifier, args: any[]) {
this.callee = callee;
this.args = args;
}
codeGen() {
return `${this.callee.codeGen()}(${this.args.join(',')})`;
}
}
export class ClazzDef implements IClazzDefine {
code: string;
constructor(code: string) {
this.code = code;
}
codeGen() {
return `${this.code}`;
}
}
export class Statement implements IStmt, Node {
children: Node[] = [];
}
export class GroupAst implements IGroupAst {
type = 'root' as const;
children: ASTItem[] = [];
childId: string[] = [];
codeGen() {
return this.children.map((child) => child.codeGen()).join('\n');
}
}
type IVarDecl = {
name: string;
val: ASTItem;
codeGen: () => string;
};
type IIdentifier = {
name: string;
codeGen: () => string;
};
type ICallExpression = {
callee: IIdentifier;
args: any[];
codeGen: () => string;
};
type IClazzDefine = {
code: string;
codeGen: () => string;
};
type IStmt = {
children: Node[];
};
type ASTItem =
| IVarDecl
| IIdentifier
| ICallExpression
| IClazzDefine
| IGroupAst;
type IAST = {
type: 'root';
children: ASTItem[];
codeGen: () => string;
};
interface IGroupAst extends IAST {
childId: string[];
}

View File

@ -0,0 +1,37 @@
import {
Controller,
Get,
HttpException,
HttpStatus,
Param,
Res,
StreamableFile,
} from '@nestjs/common';
import { Response } from 'express';
import { createReadStream, existsSync } from 'fs';
import { join } from 'path';
@Controller('code-generate')
export class CodeGenerateController {
@Get(':filename')
download(
@Param('filename') fileName: string,
@Res({ passthrough: true }) res: Response,
): StreamableFile {
if (!fileName) {
throw new HttpException(
'file name can not be undefined',
HttpStatus.BAD_REQUEST,
);
}
const filePath = join(process.cwd(), 'public', fileName + '.py');
if (!existsSync(filePath)) {
throw new HttpException(`${fileName} not found`, HttpStatus.NOT_FOUND);
}
res.set({
'Content-Type': 'text/plain',
'Content-Disposition': `attachment; filename="${fileName}.py"`,
});
return new StreamableFile(createReadStream(filePath));
}
}

View File

@ -0,0 +1,153 @@
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { CodeGenerateService, Exception } from './code-generate.service';
import { Cell, GenerateCodeDto } from './code-generate.schema';
import { Socket } from 'socket.io';
import { AST } from './ast.service';
import { UseFilters, ValidationPipe } from '@nestjs/common';
import { writeFileSync } from 'fs';
import { createHash } from 'crypto';
import { join } from 'path';
import { cwd } from 'process';
import { JwtService } from '@nestjs/jwt';
import { InjectRedis } from '@liaoliaots/nestjs-redis';
import { Redis } from 'ioredis';
import { WsExceptionFilter } from '../ws-exception/ws-exception.filter';
export enum State {
err = 'err',
finfish = 'finish',
progress = 'progress',
done = 'done',
}
@WebSocketGateway(9001, {
cors: {
origin: '*',
},
})
export class CodeGenerateGateway {
constructor(
private readonly codeGenerateService: CodeGenerateService,
private readonly ast: AST,
private readonly jwt: JwtService,
@InjectRedis() private readonly redis: Redis,
) {}
@UseFilters(WsExceptionFilter)
@SubscribeMessage('createCodeGenerate')
async create(
@MessageBody(new ValidationPipe()) schema: GenerateCodeDto,
@ConnectedSocket() client: Socket,
) {
const token = client.handshake.headers.authorization;
if (!token || !(await this.redis.exists(token))) {
client.emit('unauth', '');
client.disconnect();
return;
}
try {
const {
handshake: {
headers: { authorization },
},
} = client;
this.jwt.verifyAsync(authorization ?? '');
} catch {
client.emit('unauth', '');
client.disconnect();
return;
}
const {
meta: { start, end },
payload: { cells: cell, edges },
} = schema;
if (!cell) {
client.emitWithAck(State.err, '创建结束, 因为没有节点');
return;
}
if (!cell.length) {
client.emitWithAck(State.finfish, '创建结束, 因为图为空');
return;
}
try {
client.emitWithAck(State.progress, '检查起始节点');
this.codeGenerateService.checkStartNodes(cell, start);
client.emitWithAck(State.progress, '检查起始节点通过');
client.emitWithAck(State.progress, '检查结束节点');
this.codeGenerateService.checkEndNode(cell, end);
client.emitWithAck(State.progress, '检查结束节点通过');
} catch (e) {
const { message } = e as Error;
client.emitWithAck(State.err, message);
return;
}
client.emitWithAck(State.progress, '提取节点...');
const nodes = this.codeGenerateService.extract<Cell>(cell, (cell: any) =>
cell.shape.includes('node'),
);
client.emitWithAck(State.progress, `节点数量为: ${nodes.length}`);
if (!nodes.length) {
client.emitWithAck(State.finfish, '生成结束');
return;
}
client.emitWithAck(State.progress, `边数量为: ${edges.length}`);
if (!edges.length) {
client.emitWithAck(State.finfish, `生成结束, 边节点数量为0`);
return;
}
if (!nodes.length) {
client.emitWithAck(State.finfish, '生成结束, 节点数量为0');
return;
}
client.emitWithAck(State.progress, '标准化节点');
const standardizationNodes =
this.codeGenerateService.standardizationNode(nodes);
client.emitWithAck(State.progress, '标准化边');
const standardizationEdges = this.codeGenerateService.standardizationEdge(
edges,
standardizationNodes,
);
client.emitWithAck(State.progress, '顺序化节点');
const sequence = this.codeGenerateService.sequencingNode(
standardizationNodes,
standardizationEdges,
end,
start,
);
client.emitWithAck(State.progress, 'AST构建');
const ast = this.ast.build(sequence, standardizationNodes);
if (!ast.children) {
// transform success
client.emitWithAck(State.finfish, 'AST转码成功');
return;
}
client.emitWithAck(State.progress, 'AST转码');
const code = ast.codeGen();
client.emitWithAck(State.progress, 'AST转码成功');
const hash = createHash('sha512').update(code).digest('hex').toString();
const fileName = `${hash}-${new Date().getTime()}`;
const content = ['import paddle', 'from paddle import *', code].join('\n');
const publicPath = join(cwd(), 'public');
writeFileSync(join(publicPath, fileName + '.py'), content);
return client.emitWithAck(State.done, fileName);
}
async handleConnection(@ConnectedSocket() socket: Socket) {
const token = socket.handshake.headers.authorization;
if (!token || !(await this.redis.exists(token))) {
socket.emit('unauth', '');
socket.disconnect();
return;
}
try {
this.jwt.verifyAsync(socket.handshake.headers.authorization ?? '');
} catch {
socket.emit('unauth', '');
socket.disconnect();
return;
}
}
}

View File

@ -0,0 +1,35 @@
import { Module, OnModuleInit } from '@nestjs/common';
import { CodeGenerateService } from './code-generate.service';
import { CodeGenerateGateway } from './code-generate.gateway';
import { AST } from './ast.service';
import { CodeGenerateController } from './code-generate.controller';
import { JwtModule } from '@nestjs/jwt';
import { join } from 'path';
import { readFileSync } from 'fs';
@Module({
imports: [
JwtModule.register({
global: true,
publicKey: readFileSync(
join(process.cwd(), process.env.JWT_PUB_KEY ?? './keys/pub.key'),
).toString(),
privateKey: readFileSync(
join(process.cwd(), process.env.JWT_PRI_KEY ?? './keys/pri.key'),
).toString(),
signOptions: {
algorithm: process.env.JWT_SIGN_ALGORITHM ?? 'RS256',
expiresIn: process.env.JWT_EXPIRE_IN ?? '1 day',
},
}),
],
controllers: [CodeGenerateController],
providers: [CodeGenerateGateway, CodeGenerateService, AST],
})
export class CodeGenerateModule implements OnModuleInit {
onModuleInit() {
console.log(
join(process.cwd(), process.env.JWT_PUB_KEY ?? './keys/pub.key'),
);
}
}

View File

@ -0,0 +1,112 @@
import {
IsArray,
IsBoolean,
IsNumber,
IsObject,
IsString,
} from 'class-validator';
import { Enum, Label, Property as TProperty } from '@app/shared';
import { Layer as LayerSchema } from '../layer/layer.schema';
class Meta {
@IsString()
start: string;
@IsString()
end: string;
}
class Property implements TProperty {
id: string;
label: Label;
type: string;
default: string | number | null | boolean;
enums: Enum[];
data: any;
}
class Material {
@IsObject()
label: Label;
@IsString()
id: string;
@IsString()
desc: string;
@IsBoolean()
nn: boolean;
@IsArray()
properties: Property[];
@IsString()
mode: string;
}
export class Layer extends LayerSchema {
@IsString()
id: string;
@IsObject()
lable: Label;
@IsString()
code: string;
@IsString()
clazz: string;
@IsArray()
properties: Property[];
@IsString()
mode: string;
}
export class Cell {
@IsString()
id: string;
@IsString()
shape: string;
@IsObject()
position: {
x: number;
y: number;
};
@IsObject()
size: {
width: number;
height: number;
};
@IsObject()
attrs: object;
@IsNumber()
zIndex: number;
@IsObject()
data: Material | Layer;
@IsObject()
children?: string[];
}
export class Edge {
@IsString()
id: string;
@IsString()
shape: string;
@IsObject()
source: {
cell: string;
port: string;
};
@IsObject()
target: {
cell: string;
port: string;
};
@IsObject()
attr: object;
@IsNumber()
zIndex: number;
@IsString()
parent: string;
}
export class GenerateCodeDto {
@IsObject()
meta: Meta;
@IsObject()
payload: {
cells: (Cell | Edge)[];
edges: Edge[];
};
}

View File

@ -0,0 +1,128 @@
import { Injectable } from '@nestjs/common';
import { Cell, Edge } from './code-generate.schema';
export type StandardizationEdges = {
[source: string]: Set<string>;
};
export type StandardizationNodes = {
[id: string]: Cell;
};
export class Exception extends Error {}
@Injectable()
export class CodeGenerateService {
checkStartNodes(cells: (Cell | Edge)[], startId: string) {
const nodes = cells.filter(({ id }) => id === startId);
if (!nodes.length) {
throw new Error('找不到起始节点');
}
}
checkEndNode(cells: (Cell | Edge)[], endId: string) {
const nodes = cells.filter(({ id }) => id === endId);
if (!nodes.length) {
throw new Exception('找不到结束节点');
}
if (nodes.length > 1) {
throw new Exception('结束节点数量大于1, 请考虑合并或修改网络结构');
}
}
extract<R>(cell: (Cell | Edge)[], fn: (cell: Cell | Edge) => boolean) {
return cell.filter(fn) as unknown as R[];
}
standardizationEdge(edges: Edge[], nodes: StandardizationNodes) {
const obj: StandardizationEdges = {};
for (const edge of edges) {
const { source, target } = edge;
if (obj[target.cell]) {
obj[target.cell].add(source.cell);
} else {
obj[target.cell] = new Set([source.cell]);
}
}
return obj;
}
standardizationNode(cells: Cell[]) {
const obj: StandardizationNodes = {};
for (const cell of cells) {
obj[cell.id] = cell;
}
return obj;
}
getChildren(
id: string,
edge: StandardizationEdges,
nodes: StandardizationNodes,
) {
const childIds = edge[id];
if (!childIds || !childIds.size) {
return [];
}
const children = [];
for (const childId of childIds) {
const child = nodes[childId];
children.push(child);
}
}
/**
*
* start - end可能不一定是拓补上的有序
* ```
* --------
* v |
* --------- |
* | end | |
* --------- |
* |
* |
* |
* |
* ----------- |
* | start | |
* ----------- |
* | |
* -------
* ```
*
* ,
* ```
* | |
* |---------------|
* | start | <----- Stack Top
* |---------------|
* | end | <----- Stack Bottom
* +---------------+
```
*/
sequencingNode(
nodes: StandardizationNodes,
edges: StandardizationEdges,
endId: string,
startId: string,
) {
debugger;
const endNode = nodes[endId];
const sequence: Cell[] = [endNode];
const visitor = (id: string) => {
if (!id || id === startId) {
return;
}
const edgesArr = Array.from(edges[id] ?? []);
for (const edge of edgesArr) {
const connectedNode = nodes[edge];
if (connectedNode) {
sequence.unshift(connectedNode);
if (connectedNode.shape.includes('group')) {
for (const child of connectedNode.children) {
visitor(child);
}
}
visitor(connectedNode.id);
}
}
};
visitor(endId);
return sequence;
}
}

View File

@ -0,0 +1,17 @@
import { Cell, Layer } from './code-generate.schema';
export const layerCodeGen = (layer: Layer) => {
const { code, id, clazz, properties } = layer;
const args = properties.map(({ data }) => data);
const call = [clazz, '(', args.join(','), ')'].join('');
return [id, '=', call].join('');
};
export const nnCodeGen = (cell: Cell) => {
const {
data: { id, properties },
} = cell;
const args = properties.map(({ data }) => data);
const stack = [id, '(', args, ')'];
return stack.join('');
};

View File

@ -0,0 +1,59 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LayerService } from '../layer.service';
import { MongooseModule } from '@nestjs/mongoose';
import { Layer, LayerSchema } from '../layer.schema';
import { DbModule } from '@app/database';
describe('LayerService', () => {
let service: LayerService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
DbModule,
MongooseModule.forFeature([{ name: Layer.name, schema: LayerSchema }]),
],
providers: [LayerService],
}).compile();
service = module.get<LayerService>(LayerService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('Get All Layer', async () => {
const id = (
await service.saveLayer({
label: {
en_US: '',
zh_CN: '',
},
properties: [],
code: '',
clazz: '',
})
).id;
expect(service.findAll()).resolves.toHaveLength(1);
});
it('Create Layer', () => {
expect(service.saveLayer({} as any)).resolves.not.toThrow();
});
it('Delete Layer', async () => {
const id = (
await service.saveLayer({
label: {
en_US: '',
zh_CN: '',
},
properties: [],
code: '',
clazz: '',
})
).id;
expect(service.deleteLayer({ id })).resolves.toBeUndefined();
});
it('Delete Layer But not exists', () => {
expect(service.deleteLayer({ id: 'not exists record' })).rejects.toThrow();
});
});

View File

@ -0,0 +1,19 @@
import { IsProperties, Label, Property } from '@app/shared';
import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
export class CreateLayerDto {
@IsOptional()
@IsString({ message: 'Id should be string' })
id?: string;
@IsObject()
label: Label;
@IsString()
code: string;
@IsString()
@IsNotEmpty()
clazz: string;
@IsProperties({
message: 'properties should be array, please check field',
})
properties: Property[];
}

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