openxiangda 1.0.24 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli.js +81 -10
- package/openxiangda-skills/SKILL.md +1 -0
- package/openxiangda-skills/references/best-practices.md +17 -0
- package/openxiangda-skills/references/component-guide.md +34 -8
- package/openxiangda-skills/references/pages/publish-flow.md +26 -0
- package/openxiangda-skills/skills/openxiangda-app/SKILL.md +1 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +12 -0
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +7 -0
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +15 -0
- package/package.json +1 -1
- package/packages/sdk/src/build-source/scripts/publish-all.mjs +44 -5
- package/packages/sdk/src/build-source/scripts/utils/incremental.mjs +95 -0
- package/packages/sdk/src/build-source/scripts/utils/incremental.test.ts +62 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/design-style.md +6 -0
- package/templates/sy-lowcode-app-workspace/package.json +1 -0
package/lib/cli.js
CHANGED
|
@@ -72,7 +72,7 @@ Usage:
|
|
|
72
72
|
openxiangda workspace init [dir] [--name package-name] [--install] [--profile name --app-type APP_XXX]
|
|
73
73
|
openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--install]
|
|
74
74
|
openxiangda workspace bind --profile <name> --app-type <APP_XXX>
|
|
75
|
-
openxiangda workspace publish --profile <name> [--prune]
|
|
75
|
+
openxiangda workspace publish --profile <name> [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources] [--prune]
|
|
76
76
|
openxiangda app list [--profile name] [--json]
|
|
77
77
|
openxiangda app create <name> [--profile name] [--description text]
|
|
78
78
|
openxiangda app snapshot <APP_XXX> [--profile name] [--json]
|
|
@@ -660,6 +660,7 @@ async function workspace(args) {
|
|
|
660
660
|
}
|
|
661
661
|
|
|
662
662
|
if (subcommand === 'publish') {
|
|
663
|
+
const publishOptions = normalizeWorkspacePublishOptions(flags);
|
|
663
664
|
const state = loadProjectState();
|
|
664
665
|
const bound = state.profiles?.[profileName];
|
|
665
666
|
if (!bound?.appType) {
|
|
@@ -672,15 +673,30 @@ async function workspace(args) {
|
|
|
672
673
|
profileName,
|
|
673
674
|
`/openxiangda-api/v1/apps/${encodeURIComponent(bound.appType)}/snapshot`
|
|
674
675
|
);
|
|
675
|
-
runWorkspacePublish(profileName, profile, bound.appType);
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
676
|
+
runWorkspacePublish(profileName, profile, bound.appType, publishOptions.workspaceArgs);
|
|
677
|
+
if (publishOptions.includeResources) {
|
|
678
|
+
if (publishOptions.dryRun) {
|
|
679
|
+
const manifest = loadWorkspaceResources();
|
|
680
|
+
const validation = validateWorkspaceResources(manifest);
|
|
681
|
+
if (validation.errors.length > 0) {
|
|
682
|
+
fail(`资源配置校验失败: ${validation.errors[0]}`);
|
|
683
|
+
}
|
|
684
|
+
const target = getWorkspaceTarget(config, profileName, {});
|
|
685
|
+
const plan = await buildResourcePlan(config, target, manifest);
|
|
686
|
+
printResourcePlan(plan);
|
|
687
|
+
} else {
|
|
688
|
+
await publishResourcesForWorkspace(config, profileName, {
|
|
689
|
+
quiet: true,
|
|
690
|
+
prune: Boolean(flags.prune),
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
} else if (!publishOptions.quietResourceSkip) {
|
|
694
|
+
print('已跳过 src/resources 发布;如需同时发布资源,传 --resources。');
|
|
695
|
+
}
|
|
680
696
|
return;
|
|
681
697
|
}
|
|
682
698
|
|
|
683
|
-
fail('用法: openxiangda workspace init|bind|publish');
|
|
699
|
+
fail('用法: openxiangda workspace init|bind|publish [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources]');
|
|
684
700
|
}
|
|
685
701
|
|
|
686
702
|
async function app(args) {
|
|
@@ -2060,7 +2076,7 @@ async function commands(args) {
|
|
|
2060
2076
|
'platform add|list|use|remove',
|
|
2061
2077
|
'auth status|refresh|logout',
|
|
2062
2078
|
'env',
|
|
2063
|
-
'workspace init|bind|publish [--app-name] [--prune]',
|
|
2079
|
+
'workspace init|bind|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
|
|
2064
2080
|
'app list|create|snapshot',
|
|
2065
2081
|
'form list|create|bind|pull|publish',
|
|
2066
2082
|
'page list|publish|bind|releases|activate',
|
|
@@ -5070,7 +5086,60 @@ async function refreshProfile(config, profileName) {
|
|
|
5070
5086
|
};
|
|
5071
5087
|
}
|
|
5072
5088
|
|
|
5073
|
-
function
|
|
5089
|
+
function readStringFlag(flags, key) {
|
|
5090
|
+
const value = flags[key];
|
|
5091
|
+
if (value === undefined || value === null || value === false) return '';
|
|
5092
|
+
if (value === true) fail(`--${key} 需要提供值`);
|
|
5093
|
+
return String(value).trim();
|
|
5094
|
+
}
|
|
5095
|
+
|
|
5096
|
+
function normalizeWorkspacePublishOptions(flags) {
|
|
5097
|
+
const targetForm = readStringFlag(flags, 'form');
|
|
5098
|
+
const targetPage = readStringFlag(flags, 'page');
|
|
5099
|
+
const only = readStringFlag(flags, 'only');
|
|
5100
|
+
const since = readStringFlag(flags, 'since');
|
|
5101
|
+
const changed = Boolean(flags.changed || flags['changed-only']);
|
|
5102
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
5103
|
+
const force = Boolean(flags.force);
|
|
5104
|
+
const skipResources = Boolean(flags['skip-resources']);
|
|
5105
|
+
const includeResourcesFlag = Boolean(flags.resources || flags['include-resources']);
|
|
5106
|
+
const prune = Boolean(flags.prune);
|
|
5107
|
+
|
|
5108
|
+
if (targetForm && targetPage) {
|
|
5109
|
+
fail('workspace publish 不能同时使用 --form 和 --page,请分两次发布');
|
|
5110
|
+
}
|
|
5111
|
+
if (changed && (targetForm || targetPage || only)) {
|
|
5112
|
+
fail('workspace publish --changed 不能与 --form、--page 或 --only 同时使用');
|
|
5113
|
+
}
|
|
5114
|
+
if (since && !changed) {
|
|
5115
|
+
fail('workspace publish --since 只能与 --changed 一起使用');
|
|
5116
|
+
}
|
|
5117
|
+
if (skipResources && prune) {
|
|
5118
|
+
fail('workspace publish --skip-resources 不能与 --prune 同时使用');
|
|
5119
|
+
}
|
|
5120
|
+
|
|
5121
|
+
const workspaceArgs = [];
|
|
5122
|
+
if (targetForm) workspaceArgs.push('--form', targetForm);
|
|
5123
|
+
if (targetPage) workspaceArgs.push('--page', targetPage);
|
|
5124
|
+
if (only) workspaceArgs.push('--only', only);
|
|
5125
|
+
if (changed) workspaceArgs.push('--changed');
|
|
5126
|
+
if (since) workspaceArgs.push('--since', since);
|
|
5127
|
+
if (dryRun) workspaceArgs.push('--dry-run');
|
|
5128
|
+
if (force) workspaceArgs.push('--force');
|
|
5129
|
+
|
|
5130
|
+
const targeted = Boolean(targetForm || targetPage || only || changed);
|
|
5131
|
+
const includeResources =
|
|
5132
|
+
!skipResources && (includeResourcesFlag || prune || !targeted);
|
|
5133
|
+
|
|
5134
|
+
return {
|
|
5135
|
+
workspaceArgs,
|
|
5136
|
+
dryRun,
|
|
5137
|
+
includeResources,
|
|
5138
|
+
quietResourceSkip: !targeted || skipResources,
|
|
5139
|
+
};
|
|
5140
|
+
}
|
|
5141
|
+
|
|
5142
|
+
function runWorkspacePublish(profileName, profile, appType, publishArgs = []) {
|
|
5074
5143
|
const packageFile = path.join(process.cwd(), 'package.json');
|
|
5075
5144
|
if (!fs.existsSync(packageFile)) {
|
|
5076
5145
|
fail('当前目录没有 package.json,无法识别工作区发布脚本');
|
|
@@ -5088,7 +5157,9 @@ function runWorkspacePublish(profileName, profile, appType) {
|
|
|
5088
5157
|
|
|
5089
5158
|
const usePnpm = fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'));
|
|
5090
5159
|
const command = usePnpm ? 'pnpm' : 'npm';
|
|
5091
|
-
const args =
|
|
5160
|
+
const args = publishArgs.length
|
|
5161
|
+
? ['run', scriptName, '--', ...publishArgs]
|
|
5162
|
+
: ['run', scriptName];
|
|
5092
5163
|
const result = spawnSync(command, args, {
|
|
5093
5164
|
cwd: process.cwd(),
|
|
5094
5165
|
stdio: 'inherit',
|
|
@@ -79,6 +79,7 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
79
79
|
- If there is no `sy-lowcode-app-workspace`, create one with `openxiangda workspace init <dir>` before writing forms, pages, or JS_CODE nodes.
|
|
80
80
|
- Never treat `openxiangda form create` as page generation. It only creates a low-level platform form shell for diagnostics or for workspace publish internals. The source of user-facing pages is `sy-lowcode-app-workspace`.
|
|
81
81
|
- Publish normal form pages, workflow form pages, and custom code pages through `openxiangda workspace publish --profile <name>` from the app workspace.
|
|
82
|
+
- For routine AI edits, avoid reflexive full publish. First run `openxiangda workspace publish --profile <name> --changed --dry-run`, then use `--changed`, `--page <pageCode>`, `--form <formCode>`, or `--only pages/a,forms/b`. Use full publish only when broad shared/config changes intentionally affect many modules.
|
|
82
83
|
- Never store token data in the project directory. User tokens live in `~/.openxiangda/profiles.json`; project state lives in `.openxiangda/state.json` and stores only IDs and mappings.
|
|
83
84
|
- Use logical resource codes in local files. Platform-specific IDs such as `formUuid`, `pageId`, `workflowId`, and `automationId` must be isolated by profile.
|
|
84
85
|
- Put engineering-managed resources in `src/resources/` and use `openxiangda resource validate|plan|publish|pull`. `workspace publish` publishes workspace forms/pages first, then runs non-destructive resource upsert. Only pass `--prune` when the user explicitly wants local manifests to delete platform-side extras.
|
|
@@ -140,6 +140,23 @@ groups or backend-side JS_CODE checks.
|
|
|
140
140
|
- Keep interaction styles consistent across PC and mobile, but do not force the
|
|
141
141
|
same layout component onto both viewports.
|
|
142
142
|
|
|
143
|
+
## Library Selection
|
|
144
|
+
|
|
145
|
+
- Do not hand-write mature controls or engines. Use platform components for
|
|
146
|
+
personnel, departments, files, images, rich text, locations, and data lists.
|
|
147
|
+
- Use `antd` / `antd-mobile` for standard controls, overlays, tables, steps,
|
|
148
|
+
tabs, forms, and feedback. Avoid raw native inputs unless the behavior is
|
|
149
|
+
truly trivial and already styled by a wrapper.
|
|
150
|
+
- Use ECharts for charts and dashboards.
|
|
151
|
+
- Use GSAP for complex timeline/scroll/sequence animations; simple transitions
|
|
152
|
+
can stay in CSS. If an animation-specific skill is available, read it before
|
|
153
|
+
implementing.
|
|
154
|
+
- For drag/drop, virtual lists, calendars, spreadsheet-like input, QR/barcode,
|
|
155
|
+
or export/import, research maintained packages and official docs before
|
|
156
|
+
writing code.
|
|
157
|
+
- New dependencies should have a clear reason and a small adapter layer in the
|
|
158
|
+
app, not copied third-party internals.
|
|
159
|
+
|
|
143
160
|
## Template Catalog
|
|
144
161
|
|
|
145
162
|
- `customer-profile`: standard form schema with validation, members,
|
|
@@ -4,14 +4,40 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## 1. 核心原则
|
|
8
|
-
|
|
9
|
-
1. **平台组件优先**:所有平台组件已经接入了组织架构、文件存储、权限、多端适配等平台能力,重写一遍 = 丢能力 + 后续无法维护。
|
|
10
|
-
2. **基于 antd / antd-mobile 包装**:自定义组件必须站在 antd 之上,保证 PC 和移动端的基础体验一致。
|
|
11
|
-
3.
|
|
12
|
-
4.
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
## 1. 核心原则
|
|
8
|
+
|
|
9
|
+
1. **平台组件优先**:所有平台组件已经接入了组织架构、文件存储、权限、多端适配等平台能力,重写一遍 = 丢能力 + 后续无法维护。
|
|
10
|
+
2. **基于 antd / antd-mobile 包装**:自定义组件必须站在 antd 之上,保证 PC 和移动端的基础体验一致。
|
|
11
|
+
3. **成熟能力先调研开源方案**:图表、动画、拖拽、虚拟列表、富文本、日历、导入导出、复杂表格等,不要直接手写。先查当前项目依赖、官方文档和维护状态,再决定使用库或轻量封装。
|
|
12
|
+
4. **样式走变量与语义类**:使用 CSS 变量、Tailwind 语义类,**禁止**在 JSX 中硬编码颜色、间距、字号、圆角。
|
|
13
|
+
5. **不覆盖组件内部 class**:组件库内部类名(`ant-select-selector` 等)是私有 API,下个版本就可能变。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1.1 开源库选型规则
|
|
18
|
+
|
|
19
|
+
AI 在实现成熟交互前必须先判断是否已有可靠组件或库:
|
|
20
|
+
|
|
21
|
+
| 场景 | 优先方案 | 说明 |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| PC 表单控件、筛选、弹窗、表格、步骤条 | `antd` | 不要用原生 `<input>` / `<select>` 复刻。用 `Input`、`Input.Search`、`Select`、`DatePicker`、`Table`、`Modal`、`Drawer`、`Form` 等。 |
|
|
24
|
+
| 移动端操作、列表、弹层、Tab | `antd-mobile` | 移动端不要强行套 PC 组件。 |
|
|
25
|
+
| 平台数据录入能力 | `openxiangda` 平台字段组件 | 人员、部门、附件、图片、富文本、位置、数据管理列表优先平台组件。 |
|
|
26
|
+
| 图表、仪表盘、经营看板 | `echarts` + `echarts-for-react` | 不要手写 canvas/SVG 图表,除非只是非常小的装饰图形。 |
|
|
27
|
+
| 复杂时间轴动画、编排动画、滚动动画 | `gsap` | 需要动画库时先查 GSAP 官方文档;如果当前 Agent 有动画/GSAP skill,先读取该 skill。简单 hover/transition 用 CSS 即可。 |
|
|
28
|
+
| 拖拽排序、看板、可拖卡片 | `@dnd-kit/*` | 不要自己处理 pointer/mouse/touch 全套事件。 |
|
|
29
|
+
| 大列表虚拟滚动 | antd Table virtual / `rc-virtual-list` | 不要一次渲染几千行 DOM。 |
|
|
30
|
+
| 日期时间处理 | `dayjs` + antd 日期组件 | 不要手写日期解析、时区格式化。 |
|
|
31
|
+
| 富文本 | 平台 `EditorField` 或成熟编辑器封装 | 不要用 contenteditable 从零做编辑器。 |
|
|
32
|
+
|
|
33
|
+
选型流程:
|
|
34
|
+
|
|
35
|
+
1. 先看模板 `package.json` 已有依赖,已有依赖优先复用。
|
|
36
|
+
2. 没有依赖但功能成熟复杂时,调研官方文档、维护状态、包体积和授权,再通过包管理器加入。
|
|
37
|
+
3. 页面代码只写业务适配层,不复制第三方库内部逻辑。
|
|
38
|
+
4. 新增依赖后,在提交说明或实现注释中保留一句选择原因。
|
|
39
|
+
|
|
40
|
+
---
|
|
15
41
|
|
|
16
42
|
## 2. 必须使用平台组件(重做即丢能力)
|
|
17
43
|
|
|
@@ -6,6 +6,32 @@ Preferred publish command:
|
|
|
6
6
|
openxiangda workspace publish --profile <name>
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
For day-to-day AI edits, prefer a targeted publish:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Preview what git-touched forms/pages would publish.
|
|
13
|
+
openxiangda workspace publish --profile <name> --changed --dry-run
|
|
14
|
+
|
|
15
|
+
# Publish only git-touched src/forms/* and src/pages/* modules.
|
|
16
|
+
openxiangda workspace publish --profile <name> --changed
|
|
17
|
+
|
|
18
|
+
# Publish one code page or one form when the changed module is known.
|
|
19
|
+
openxiangda workspace publish --profile <name> --page dashboard
|
|
20
|
+
openxiangda workspace publish --profile <name> --form customer
|
|
21
|
+
|
|
22
|
+
# Publish several explicit modules.
|
|
23
|
+
openxiangda workspace publish --profile <name> --only pages/dashboard,forms/customer
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Do not use full workspace publish as a reflex. Full publish is only appropriate
|
|
27
|
+
when many modules were intentionally changed, shared/config changes affect many
|
|
28
|
+
pages, or the incremental cache needs a repair pass. For a single page edit,
|
|
29
|
+
publish that page.
|
|
30
|
+
|
|
31
|
+
Targeted publish skips `src/resources` by default. Add `--resources` if resource
|
|
32
|
+
manifests changed too, or run `openxiangda resource plan|publish` separately.
|
|
33
|
+
Use `--skip-resources` when you explicitly want forms/pages only.
|
|
34
|
+
|
|
9
35
|
The CLI injects:
|
|
10
36
|
|
|
11
37
|
- `OPENXIANGDA_PROFILE`
|
|
@@ -55,6 +55,7 @@ openxiangda workspace bind --profile <name> --app-type APP_XXX
|
|
|
55
55
|
- Store platform-specific IDs only in `.openxiangda/state.json`.
|
|
56
56
|
- Use logical local codes for forms, pages, workflows, automations, and menus.
|
|
57
57
|
- Before scaffolding a new business app, read `../../references/best-practices.md` and pick an architecture from the initialized `examples/best-practices/` catalog. Do not generate a large single-file app page when a template pattern already covers the scenario.
|
|
58
|
+
- When publishing after app edits, prefer targeted commands (`workspace publish --changed --dry-run`, then `--changed`, `--page`, `--form`, or `--only`). Do not publish all forms/pages just because one page changed.
|
|
58
59
|
- Use `openxiangda app snapshot <APP_XXX> --profile <name> --json` before changing an existing app.
|
|
59
60
|
|
|
60
61
|
## Resource State
|
|
@@ -142,6 +142,18 @@ openxiangda workspace publish --profile prod
|
|
|
142
142
|
|
|
143
143
|
The CLI checks that the target profile is logged in and bound to an app. It then runs the workspace publish script with these environment variables:
|
|
144
144
|
|
|
145
|
+
For normal development, inspect and publish only changed or targeted modules:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
openxiangda workspace publish --profile dev --changed --dry-run
|
|
149
|
+
openxiangda workspace publish --profile dev --changed
|
|
150
|
+
openxiangda workspace publish --profile dev --page dashboard
|
|
151
|
+
openxiangda workspace publish --profile dev --form customer
|
|
152
|
+
openxiangda workspace publish --profile dev --only pages/dashboard,forms/customer
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Do not run a full publish automatically after a single page or form edit. Full publish is for intentional broad changes, shared/config changes that affect many modules, or cache repair. Targeted publish skips resources by default; pass `--resources` or run `openxiangda resource publish` when resource manifests changed.
|
|
156
|
+
|
|
145
157
|
- `OPENXIANGDA_PROFILE`
|
|
146
158
|
- `OPENXIANGDA_BASE_URL`
|
|
147
159
|
- `OPENXIANGDA_ACCESS_TOKEN`
|
|
@@ -39,6 +39,12 @@ Then publish:
|
|
|
39
39
|
openxiangda workspace publish --profile <name>
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
For a single form edit, publish that form only:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
openxiangda workspace publish --profile <name> --form <formCode>
|
|
46
|
+
```
|
|
47
|
+
|
|
42
48
|
The workspace publish script receives `OPENXIANGDA_BASE_URL`, `OPENXIANGDA_ACCESS_TOKEN`, and `OPENXIANGDA_APP_TYPE` from the CLI. It creates/binds the platform form shell only when needed, syncs form metadata, builds the React form page bundle, uploads assets, and registers the bundle.
|
|
43
49
|
|
|
44
50
|
## Low-level Commands
|
|
@@ -77,5 +83,6 @@ Read these references only when writing or reviewing schema:
|
|
|
77
83
|
- Put validation on field-level `rules`; never generate old top-level validation arrays under `schema.rules`. Top-level `rules` is only for `FormEffect[]` with `when` and `then`.
|
|
78
84
|
- Always provide `options` for option components. `SelectField`, `MultiSelectField`, `RadioField`, `CheckboxField`, and `CascadeSelectField` must not be emitted with `options` undefined. See `../../references/component-guide.md` for the full list and `../../references/platform-data-model.md` for the `{label, value}` storage contract.
|
|
79
85
|
- Workflow form pages use the same workspace form structure plus workflow v3 configuration; publish the form page bundle through workspace publish before treating it as complete.
|
|
86
|
+
- After editing one form, use `workspace publish --form <formCode>` or preview `--changed --dry-run`; do not run full workspace publish by default.
|
|
80
87
|
- Use `openxiangda form pull` or app snapshot before overwriting an existing form.
|
|
81
88
|
- Before assuming a value shape (e.g. dates, attachments, member fields, option fields), verify against `../../references/platform-data-model.md` instead of guessing.
|
|
@@ -16,6 +16,19 @@ cd /path/to/sy-lowcode-app-workspace
|
|
|
16
16
|
openxiangda workspace publish --profile <name>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
For a page-only edit, prefer targeted publish:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
openxiangda workspace publish --profile <name> --page <pageCode>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If multiple files changed and the target is not obvious, preview first:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
openxiangda workspace publish --profile <name> --changed --dry-run
|
|
29
|
+
openxiangda workspace publish --profile <name> --changed
|
|
30
|
+
```
|
|
31
|
+
|
|
19
32
|
If the workspace does not exist yet, initialize it first:
|
|
20
33
|
|
|
21
34
|
```bash
|
|
@@ -61,8 +74,10 @@ Read these references only when editing page code:
|
|
|
61
74
|
- Store live `pageId`, `routeKey`, and `legacyFormUuid` under the current profile only.
|
|
62
75
|
- Use `openxiangda/runtime` for platform data access instead of hardcoding backend URLs in page code.
|
|
63
76
|
- For reminders, alerts, and business messages, declare `src/resources/notifications/` first and call `sdk.notification`; do not hardcode notification API URLs.
|
|
77
|
+
- Before hand-writing mature UI behavior, consult `../../references/component-guide.md` and use established libraries: platform components for platform data fields, antd/antd-mobile for controls and overlays, ECharts for charts, GSAP for complex animation timelines, and maintained packages such as dnd-kit for drag/drop. Do not rebuild mature controls with raw DOM/native inputs.
|
|
64
78
|
- Named imports from `@ant-design/icons` are supported by the `openxiangda` workspace build proxy, which enumerates icon module exports at runtime.
|
|
65
79
|
- Publish through `openxiangda workspace publish --profile <name>` unless there is a specific repair reason to call `page publish` directly.
|
|
80
|
+
- After editing one code page, publish with `--page <pageCode>` instead of triggering all forms/pages. Use `--changed --dry-run` before `--changed` when git touched several modules.
|
|
66
81
|
- Do not create custom code pages by writing platform schema directly. The source is React workspace code plus `page.config.ts`.
|
|
67
82
|
- Do not scatter hardcoded `/view/...&isRenderNav=false` URLs through page code. Use the runtime navigation API or the local route helper generated for the app shell.
|
|
68
83
|
- Platform menus should bind only the formal app-shell code page for user-facing entry points. Original forms, workflows, and native view pages may remain as development / maintenance resources or permission targets, but should not become the product navigation shell.
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
normalizeOnly,
|
|
18
18
|
planIncrementalPublish,
|
|
19
19
|
printPlan,
|
|
20
|
+
resolveGitChangedWorkspaceTargets,
|
|
20
21
|
} from "./utils/incremental.mjs";
|
|
21
22
|
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
@@ -36,11 +37,8 @@ const dryRun = Boolean(args["dry-run"]);
|
|
|
36
37
|
const targetForm = args.form || "";
|
|
37
38
|
const targetPage = args.page || "";
|
|
38
39
|
const force = Boolean(args.force);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
: targetPage
|
|
42
|
-
? [`pages/${targetPage}`]
|
|
43
|
-
: normalizeOnly(args.only);
|
|
40
|
+
const changedOnly = Boolean(args.changed || args["changed-only"]);
|
|
41
|
+
const changedSince = args.since || "HEAD";
|
|
44
42
|
|
|
45
43
|
if (args.help || args.h) {
|
|
46
44
|
console.log(`
|
|
@@ -54,6 +52,8 @@ publish-all - 同步、构建、上传并注册应用工作区产物
|
|
|
54
52
|
--form <name> 只发布指定表单
|
|
55
53
|
--page <name> 只发布指定代码页
|
|
56
54
|
--only <list> 只发布指定模块,如 forms/customer,pages/dashboard
|
|
55
|
+
--changed 只发布 git 变更触达的 src/forms/* 和 src/pages/*
|
|
56
|
+
--since <ref> 配合 --changed 使用,默认 HEAD
|
|
57
57
|
--force 忽略增量缓存,强制发布
|
|
58
58
|
--help, -h 显示帮助信息
|
|
59
59
|
`);
|
|
@@ -64,6 +64,45 @@ if (targetForm && targetPage) {
|
|
|
64
64
|
console.error("❌ --form 和 --page 不能同时使用,请分两次发布");
|
|
65
65
|
process.exit(1);
|
|
66
66
|
}
|
|
67
|
+
if (changedOnly && (targetForm || targetPage || args.only)) {
|
|
68
|
+
console.error("❌ --changed 不能与 --form、--page 或 --only 同时使用");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const changedTargets = changedOnly
|
|
73
|
+
? resolveGitChangedWorkspaceTargets({ since: changedSince })
|
|
74
|
+
: null;
|
|
75
|
+
if (changedTargets && !changedTargets.available) {
|
|
76
|
+
console.error(`❌ 无法读取 git 变更: ${changedTargets.error}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
if (changedTargets?.globalFiles?.length) {
|
|
80
|
+
console.warn(
|
|
81
|
+
`[publish] --changed 检测到 shared/config 变更: ${changedTargets.globalFiles.join(", ")}`,
|
|
82
|
+
);
|
|
83
|
+
console.warn(
|
|
84
|
+
"[publish] 将只发布直接变更的 forms/pages;若 shared 改动影响其他模块,请显式使用 --only 或不传 --changed。",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (changedTargets?.resourceFiles?.length) {
|
|
88
|
+
console.warn(
|
|
89
|
+
`[publish] --changed 检测到资源变更: ${changedTargets.resourceFiles.join(", ")}`,
|
|
90
|
+
);
|
|
91
|
+
console.warn("[publish] publish-all 只处理 forms/pages;资源请通过 openxiangda resource plan|publish 处理。");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const only = changedOnly
|
|
95
|
+
? changedTargets.only
|
|
96
|
+
: targetForm
|
|
97
|
+
? [`forms/${targetForm}`]
|
|
98
|
+
: targetPage
|
|
99
|
+
? [`pages/${targetPage}`]
|
|
100
|
+
: normalizeOnly(args.only);
|
|
101
|
+
|
|
102
|
+
if (changedOnly && only.length === 0) {
|
|
103
|
+
console.log("✅ 没有检测到 src/forms 或 src/pages 的 git 变更,跳过发布");
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
67
106
|
|
|
68
107
|
const buildId = process.env.APP_BUILD_ID || createBuildId();
|
|
69
108
|
const childEnv = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { rootDir } from "./load-config.mjs";
|
|
@@ -124,6 +125,100 @@ export function normalizeOnly(value) {
|
|
|
124
125
|
.filter(Boolean);
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
function runGit(args) {
|
|
129
|
+
const result = spawnSync("git", args, {
|
|
130
|
+
cwd: rootDir,
|
|
131
|
+
encoding: "utf-8",
|
|
132
|
+
});
|
|
133
|
+
if (result.error || result.status !== 0) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: result.error?.message || result.stderr || result.stdout || "git command failed",
|
|
137
|
+
stdout: "",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
error: "",
|
|
143
|
+
stdout: result.stdout || "",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function uniqueSorted(values) {
|
|
148
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseGitNameOnly(stdout) {
|
|
152
|
+
return String(stdout || "")
|
|
153
|
+
.split(/\r?\n/)
|
|
154
|
+
.map((item) => item.trim().replace(/\\/g, "/"))
|
|
155
|
+
.filter(Boolean);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function workspaceTargetForFile(filePath) {
|
|
159
|
+
const matched = filePath.match(/^src\/(forms|pages)\/([^/]+)\//);
|
|
160
|
+
if (!matched) return null;
|
|
161
|
+
return `${matched[1]}/${matched[2]}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isGlobalWorkspaceFile(filePath) {
|
|
165
|
+
return (
|
|
166
|
+
filePath === "package.json" ||
|
|
167
|
+
filePath === "pnpm-lock.yaml" ||
|
|
168
|
+
filePath === "package-lock.json" ||
|
|
169
|
+
filePath === "yarn.lock" ||
|
|
170
|
+
filePath === "app-workspace.config.ts" ||
|
|
171
|
+
filePath === "tailwind.config.cjs" ||
|
|
172
|
+
filePath === "postcss.config.cjs" ||
|
|
173
|
+
filePath === "vite.config.ts" ||
|
|
174
|
+
filePath === "tsconfig.json" ||
|
|
175
|
+
filePath === "src/index.css" ||
|
|
176
|
+
filePath.startsWith("src/shared/")
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function resolveGitChangedWorkspaceTargets(options = {}) {
|
|
181
|
+
const since = options.since || "HEAD";
|
|
182
|
+
const insideWorkTree = runGit(["rev-parse", "--is-inside-work-tree"]);
|
|
183
|
+
if (!insideWorkTree.ok || insideWorkTree.stdout.trim() !== "true") {
|
|
184
|
+
return {
|
|
185
|
+
available: false,
|
|
186
|
+
error: insideWorkTree.error || "not a git worktree",
|
|
187
|
+
files: [],
|
|
188
|
+
only: [],
|
|
189
|
+
resourceFiles: [],
|
|
190
|
+
globalFiles: [],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const changed = runGit(["diff", "--name-only", "--relative", since, "--"]);
|
|
195
|
+
if (!changed.ok) {
|
|
196
|
+
return {
|
|
197
|
+
available: false,
|
|
198
|
+
error: changed.error,
|
|
199
|
+
files: [],
|
|
200
|
+
only: [],
|
|
201
|
+
resourceFiles: [],
|
|
202
|
+
globalFiles: [],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const untracked = runGit(["ls-files", "--others", "--exclude-standard"]);
|
|
207
|
+
const files = uniqueSorted([
|
|
208
|
+
...parseGitNameOnly(changed.stdout),
|
|
209
|
+
...(untracked.ok ? parseGitNameOnly(untracked.stdout) : []),
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
available: true,
|
|
214
|
+
error: "",
|
|
215
|
+
files,
|
|
216
|
+
only: uniqueSorted(files.map(workspaceTargetForFile).filter(Boolean)),
|
|
217
|
+
resourceFiles: files.filter((item) => item.startsWith("src/resources/")),
|
|
218
|
+
globalFiles: files.filter(isGlobalWorkspaceFile),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
127
222
|
function createIncrementalPlan(modules, options = {}, cache = null) {
|
|
128
223
|
const only = normalizeOnly(options.only);
|
|
129
224
|
const selected = only.length
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
4
5
|
|
|
5
6
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
7
|
|
|
@@ -31,6 +32,24 @@ function createWorkspace() {
|
|
|
31
32
|
return dir;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function runGit(workspaceRoot: string, args: string[]) {
|
|
36
|
+
const result = spawnSync("git", args, {
|
|
37
|
+
cwd: workspaceRoot,
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
});
|
|
40
|
+
if (result.status !== 0) {
|
|
41
|
+
throw new Error(`git ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function commitWorkspace(workspaceRoot: string) {
|
|
46
|
+
runGit(workspaceRoot, ["init"]);
|
|
47
|
+
runGit(workspaceRoot, ["config", "user.email", "test@example.com"]);
|
|
48
|
+
runGit(workspaceRoot, ["config", "user.name", "Test User"]);
|
|
49
|
+
runGit(workspaceRoot, ["add", "."]);
|
|
50
|
+
runGit(workspaceRoot, ["commit", "-m", "initial"]);
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
async function loadIncremental(rootDir: string) {
|
|
35
54
|
const previousRoot = process.env.LOWCODE_WORKSPACE_ROOT;
|
|
36
55
|
process.env.LOWCODE_WORKSPACE_ROOT = rootDir;
|
|
@@ -75,4 +94,47 @@ describe("incremental publish cache", () => {
|
|
|
75
94
|
|
|
76
95
|
expect(incremental.planIncrementalPublish(modules).changed).toHaveLength(0);
|
|
77
96
|
});
|
|
97
|
+
|
|
98
|
+
it("resolves git changed files to workspace publish targets", async () => {
|
|
99
|
+
const workspaceRoot = createWorkspace();
|
|
100
|
+
commitWorkspace(workspaceRoot);
|
|
101
|
+
|
|
102
|
+
fs.writeFileSync(
|
|
103
|
+
path.join(workspaceRoot, "src", "pages", "dashboard", "index.tsx"),
|
|
104
|
+
"export default function Dashboard() { return 'changed'; }\n",
|
|
105
|
+
"utf-8",
|
|
106
|
+
);
|
|
107
|
+
fs.mkdirSync(path.join(workspaceRoot, "src", "forms", "customer"), {
|
|
108
|
+
recursive: true,
|
|
109
|
+
});
|
|
110
|
+
fs.writeFileSync(
|
|
111
|
+
path.join(workspaceRoot, "src", "forms", "customer", "schema.ts"),
|
|
112
|
+
"export default { code: 'customer' };\n",
|
|
113
|
+
"utf-8",
|
|
114
|
+
);
|
|
115
|
+
fs.mkdirSync(path.join(workspaceRoot, "src", "resources"), {
|
|
116
|
+
recursive: true,
|
|
117
|
+
});
|
|
118
|
+
fs.writeFileSync(
|
|
119
|
+
path.join(workspaceRoot, "src", "resources", "menus.json"),
|
|
120
|
+
"[]\n",
|
|
121
|
+
"utf-8",
|
|
122
|
+
);
|
|
123
|
+
fs.mkdirSync(path.join(workspaceRoot, "src", "shared"), {
|
|
124
|
+
recursive: true,
|
|
125
|
+
});
|
|
126
|
+
fs.writeFileSync(
|
|
127
|
+
path.join(workspaceRoot, "src", "shared", "format.ts"),
|
|
128
|
+
"export const format = String;\n",
|
|
129
|
+
"utf-8",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const incremental = await loadIncremental(workspaceRoot);
|
|
133
|
+
const changed = incremental.resolveGitChangedWorkspaceTargets();
|
|
134
|
+
|
|
135
|
+
expect(changed.available).toBe(true);
|
|
136
|
+
expect(changed.only).toEqual(["forms/customer", "pages/dashboard"]);
|
|
137
|
+
expect(changed.resourceFiles).toEqual(["src/resources/menus.json"]);
|
|
138
|
+
expect(changed.globalFiles).toEqual(["src/shared/format.ts"]);
|
|
139
|
+
});
|
|
78
140
|
});
|
|
@@ -24,6 +24,12 @@ OpenXiangda business apps should feel like focused operational tools.
|
|
|
24
24
|
|
|
25
25
|
- Use platform CSS variables, Tailwind semantic classes, Ant Design, and
|
|
26
26
|
antd-mobile.
|
|
27
|
+
- Use mature packages for mature interactions: antd controls instead of native
|
|
28
|
+
inputs, ECharts for charts, GSAP for complex animation timelines, and
|
|
29
|
+
maintained drag/drop or virtual-list libraries when those behaviors are
|
|
30
|
+
required.
|
|
31
|
+
- Research package docs and current maintenance before adding a new dependency;
|
|
32
|
+
write business adapters instead of copying library internals.
|
|
27
33
|
- Keep page styles in `styles.css`; avoid large inline style objects.
|
|
28
34
|
- Keep reusable visual states in shared components: status tags, query states,
|
|
29
35
|
confirmation triggers, and operation timelines.
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"register": "lowcode-workspace register",
|
|
19
19
|
"register-bundle": "lowcode-workspace register",
|
|
20
20
|
"publish:all": "lowcode-workspace publish-all",
|
|
21
|
+
"publish:changed": "lowcode-workspace publish-all --changed",
|
|
21
22
|
"openxiangda:publish": "lowcode-workspace publish-all",
|
|
22
23
|
"ai:update": "pnpm dlx openxiangda@latest lowcode-workspace update --channel latest",
|
|
23
24
|
"ai:migrate": "pnpm dlx openxiangda@latest lowcode-workspace migrate",
|