openxiangda 1.0.104 → 1.0.105

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.
@@ -0,0 +1,28 @@
1
+ function buildOpenXiangdaFunctionInvokeRequest(appType, functionCode, body) {
2
+ return {
3
+ method: 'POST',
4
+ path: `/openxiangda-api/v1/apps/${encodeURIComponent(appType)}/functions/${encodeURIComponent(functionCode)}/invoke`,
5
+ body,
6
+ strictEnvelope: true,
7
+ };
8
+ }
9
+
10
+ function buildLegacyFunctionInvokeRequest(appType, functionCode, body) {
11
+ return {
12
+ method: 'POST',
13
+ path: `/${encodeURIComponent(appType)}/v1/functions/${encodeURIComponent(functionCode)}/invoke.json`,
14
+ body,
15
+ strictEnvelope: true,
16
+ };
17
+ }
18
+
19
+ function isHttpNotFound(error) {
20
+ const status = Number(error?.status || error?.statusCode || error?.response?.status);
21
+ return status === 404 || String(error?.message || '').includes('HTTP 404');
22
+ }
23
+
24
+ module.exports = {
25
+ buildLegacyFunctionInvokeRequest,
26
+ buildOpenXiangdaFunctionInvokeRequest,
27
+ isHttpNotFound,
28
+ };
package/lib/cli.js CHANGED
@@ -20,6 +20,11 @@ const {
20
20
  } = require('./config');
21
21
  const { requestJson } = require('./http');
22
22
  const { getSkillStatusReport, installSkills } = require('./skills');
23
+ const {
24
+ buildLegacyFunctionInvokeRequest,
25
+ buildOpenXiangdaFunctionInvokeRequest,
26
+ isHttpNotFound,
27
+ } = require('./app-function-invoke');
23
28
  const { assertCanInitializeWorkspace, initWorkspace } = require('./workspace-init');
24
29
  const { bootstrapWorkspace } = require('./workspace-bootstrap');
25
30
  const {
@@ -31,6 +36,7 @@ const {
31
36
  renderDesignTemplate,
32
37
  renderResourceExplain,
33
38
  } = require('./design-gates');
39
+ const { buildDesignReview, renderDesignReview } = require('./design-review');
34
40
  const {
35
41
  fail,
36
42
  formatFetchError,
@@ -104,7 +110,7 @@ Usage:
104
110
  openxiangda platform use <name>
105
111
  openxiangda auth status|refresh|logout [--profile name]
106
112
  openxiangda doctor [--profile name] [--app-type APP_XXX] [--json]
107
- openxiangda design gates|template [--topic code] [--json]
113
+ openxiangda design gates|template|review [--topic code] [--json]
108
114
  openxiangda env [--profile name]
109
115
  openxiangda workspace init [dir] [--name package-name] [--runtime legacy|react-spa] [--install] [--profile name --app-type APP_XXX]
110
116
  openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--runtime legacy|react-spa] [--install]
@@ -475,7 +481,24 @@ async function design(args) {
475
481
  return;
476
482
  }
477
483
 
478
- fail('用法: openxiangda design gates|template [--topic code] [--json]');
484
+ if (subcommand === 'review') {
485
+ const hasResources = fs.existsSync(path.join(process.cwd(), 'src', 'resources'));
486
+ const manifest = hasResources ? loadWorkspaceResources() : null;
487
+ const validation = manifest ? validateWorkspaceResources(manifest) : {
488
+ errors: [],
489
+ warnings: ['未发现 src/resources,无法审查资源 manifest'],
490
+ };
491
+ const result = buildDesignReview({
492
+ cwd: process.cwd(),
493
+ manifest,
494
+ validation,
495
+ });
496
+ if (flags.json) return writeJson(result);
497
+ print(renderDesignReview(result));
498
+ return;
499
+ }
500
+
501
+ fail('用法: openxiangda design gates|template|review [--topic code] [--json]');
479
502
  }
480
503
 
481
504
  async function doctor(args) {
@@ -2664,12 +2687,35 @@ async function appFunction(args) {
2664
2687
  const [functionCode] = positional;
2665
2688
  if (!functionCode) fail('用法: openxiangda function invoke <functionCode> [--body-json file|json]');
2666
2689
  const body = readDirectJsonBody(flags, 'function invoke', { optional: true });
2667
- return runDirectRequest(config, target, flags, {
2668
- method: 'POST',
2669
- path: `/${encodeURIComponent(target.appType)}/v1/functions/${encodeURIComponent(functionCode)}/invoke.json`,
2670
- body,
2671
- strictEnvelope: true,
2672
- });
2690
+ const openXiangdaApiRequest = buildOpenXiangdaFunctionInvokeRequest(
2691
+ target.appType,
2692
+ functionCode,
2693
+ body
2694
+ );
2695
+ if (flags['dry-run']) {
2696
+ return runDirectRequest(config, target, flags, openXiangdaApiRequest);
2697
+ }
2698
+ try {
2699
+ const data = await requestWithAuth(
2700
+ config,
2701
+ target.profileName,
2702
+ openXiangdaApiRequest.path,
2703
+ {
2704
+ method: openXiangdaApiRequest.method,
2705
+ body: openXiangdaApiRequest.body,
2706
+ strictEnvelope: true,
2707
+ }
2708
+ );
2709
+ return outputDirectResult(data, flags);
2710
+ } catch (error) {
2711
+ if (!isHttpNotFound(error)) throw error;
2712
+ return runDirectRequest(
2713
+ config,
2714
+ target,
2715
+ flags,
2716
+ buildLegacyFunctionInvokeRequest(target.appType, functionCode, body)
2717
+ );
2718
+ }
2673
2719
  }
2674
2720
 
2675
2721
  return directResourceCrud(config, target, {
@@ -4044,7 +4090,7 @@ async function commands(args) {
4044
4090
  'platform add|list|use|remove',
4045
4091
  'auth status|refresh|logout',
4046
4092
  'doctor [--profile name] [--app-type APP_XXX]',
4047
- 'design gates|template [--topic code]',
4093
+ 'design gates|template|review [--topic code]',
4048
4094
  'env',
4049
4095
  'workspace init|bind|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
4050
4096
  'app list|create|snapshot',
@@ -309,7 +309,7 @@ function getDesignTopicCatalog() {
309
309
  function renderDesignHelp() {
310
310
  const catalog = getDesignTopicCatalog();
311
311
  const lines = [
312
- '用法: openxiangda design gates|template [--topic code[,code...]] [--json]',
312
+ '用法: openxiangda design gates|template|review [--topic code[,code...]] [--json]',
313
313
  '',
314
314
  catalog.hardRule,
315
315
  '',
@@ -0,0 +1,220 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const SOURCE_DIRS = [
5
+ path.join('src', 'app'),
6
+ path.join('src', 'pages'),
7
+ path.join('src', 'forms'),
8
+ path.join('src', 'runtime'),
9
+ ];
10
+ const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
11
+ const SKIP_DIRS = new Set([
12
+ 'node_modules',
13
+ '.pnpm',
14
+ 'dist',
15
+ '.vite',
16
+ '.vite-temp',
17
+ '.cache',
18
+ 'coverage',
19
+ ]);
20
+
21
+ function buildDesignReview(options = {}) {
22
+ const cwd = options.cwd || process.cwd();
23
+ const manifest = options.manifest || null;
24
+ const validation = options.validation || null;
25
+ const result = {
26
+ cwd,
27
+ checkedAt: new Date().toISOString(),
28
+ passed: true,
29
+ summary: {
30
+ errors: 0,
31
+ warnings: 0,
32
+ suggestions: 0,
33
+ },
34
+ errors: [],
35
+ warnings: [],
36
+ suggestions: [],
37
+ };
38
+
39
+ addResourceValidationFindings(result, validation);
40
+ reviewPublicAccess(result, manifest);
41
+ reviewSourceFiles(result, cwd);
42
+ reviewAcceptanceArtifacts(result, cwd);
43
+
44
+ result.summary.errors = result.errors.length;
45
+ result.summary.warnings = result.warnings.length;
46
+ result.summary.suggestions = result.suggestions.length;
47
+ result.passed = result.errors.length === 0;
48
+ return result;
49
+ }
50
+
51
+ function renderDesignReview(result) {
52
+ const lines = [
53
+ 'OpenXiangda design review',
54
+ `cwd: ${result.cwd}`,
55
+ `status: ${result.passed ? 'passed' : 'needs attention'}`,
56
+ `summary: ${result.summary.errors} errors, ${result.summary.warnings} warnings, ${result.summary.suggestions} suggestions`,
57
+ ];
58
+ appendFindings(lines, 'Errors', result.errors);
59
+ appendFindings(lines, 'Warnings', result.warnings);
60
+ appendFindings(lines, 'Suggestions', result.suggestions);
61
+ return `${lines.join('\n')}\n`;
62
+ }
63
+
64
+ function appendFindings(lines, title, findings) {
65
+ if (!findings.length) return;
66
+ lines.push('', `${title}:`);
67
+ for (const finding of findings) {
68
+ lines.push(`- [${finding.code}] ${finding.message}`);
69
+ if (finding.file) lines.push(` file: ${finding.file}`);
70
+ }
71
+ }
72
+
73
+ function addResourceValidationFindings(result, validation) {
74
+ for (const message of validation?.errors || []) {
75
+ result.errors.push({
76
+ code: 'resource-validation-error',
77
+ message,
78
+ });
79
+ }
80
+ for (const message of validation?.warnings || []) {
81
+ result.warnings.push({
82
+ code: 'resource-validation-warning',
83
+ message,
84
+ });
85
+ }
86
+ }
87
+
88
+ function reviewPublicAccess(result, manifest) {
89
+ if (!manifest) return;
90
+ const policies = manifest.publicAccessPolicies || [];
91
+ const policyCodes = new Set(policies.map(policy => String(policy.code || '').trim()).filter(Boolean));
92
+ const policyRouteCodes = new Set(policies.map(policy => String(policy.routeCode || '').trim()).filter(Boolean));
93
+
94
+ for (const route of manifest.routes || []) {
95
+ if (route.__invalid) continue;
96
+ const publicAccess = String(route.publicAccess || 'none');
97
+ if (publicAccess !== 'guest' && publicAccess !== 'ticket') continue;
98
+ const routeLabel = route.code || route.pathPattern || route.path || route.__source || 'public route';
99
+ const policyCode = String(route.publicPolicyCode || route.policyCode || '').trim();
100
+ if (policyCode && !policyCodes.has(policyCode)) {
101
+ result.errors.push({
102
+ code: 'public-route-missing-policy',
103
+ message: `公开路由 ${routeLabel} 引用了不存在的 public-access policy: ${policyCode}`,
104
+ file: route.__source,
105
+ });
106
+ continue;
107
+ }
108
+ if (!policyCode && route.code && !policyRouteCodes.has(String(route.code))) {
109
+ result.warnings.push({
110
+ code: 'public-route-unbound-policy',
111
+ message: `公开路由 ${routeLabel} 未声明 publicPolicyCode,也没有 policy.routeCode 绑定它`,
112
+ file: route.__source,
113
+ });
114
+ }
115
+ }
116
+
117
+ for (const policy of policies) {
118
+ if (policy.__invalid) continue;
119
+ const grants = policy.grants && typeof policy.grants === 'object' ? policy.grants : null;
120
+ const grantCount = grants
121
+ ? ['forms', 'dataViews', 'functions', 'connectors'].reduce(
122
+ (count, key) => count + (Array.isArray(grants[key]) ? grants[key].length : 0),
123
+ 0
124
+ )
125
+ : 0;
126
+ if (grantCount === 0) {
127
+ result.warnings.push({
128
+ code: 'public-policy-empty-grants',
129
+ message: `公开策略 ${policy.code || policy.__source} 没有显式 grants;公开页调用 form/dataView/function/connector 会被后端拒绝`,
130
+ file: policy.__source,
131
+ });
132
+ }
133
+ }
134
+ }
135
+
136
+ function reviewSourceFiles(result, cwd) {
137
+ for (const filePath of listSourceFiles(cwd)) {
138
+ const relativeFile = path.relative(cwd, filePath);
139
+ let source = '';
140
+ try {
141
+ source = fs.readFileSync(filePath, 'utf8');
142
+ } catch {
143
+ continue;
144
+ }
145
+ const lineCount = source.split(/\r?\n/).length;
146
+ if (relativeFile.startsWith(path.join('src', 'pages')) && lineCount > 600) {
147
+ result.warnings.push({
148
+ code: 'large-page-module',
149
+ message: `页面文件超过 600 行,建议拆分为 domain/service/hooks/components 后再交给 AI 迭代`,
150
+ file: relativeFile,
151
+ });
152
+ }
153
+ if (/\?publicAccess=guest|publicAccess=guest|isRenderNav|workbench/i.test(source)) {
154
+ result.errors.push({
155
+ code: 'legacy-public-runtime-pattern',
156
+ message: '发现旧公开访问或旧 workbench/runtime 参数;React SPA 新公开页应使用 routes + public-access + PublicAccessGate',
157
+ file: relativeFile,
158
+ });
159
+ }
160
+ if (/<(?:input|select|textarea)\b/i.test(source)) {
161
+ result.warnings.push({
162
+ code: 'native-form-control',
163
+ message: 'AI 编写的页面/表单代码不应直接使用原生表单控件,优先使用 OpenXiangda/antd/antd-mobile 封装',
164
+ file: relativeFile,
165
+ });
166
+ }
167
+ if (/(pageSize|limit)\s*[:=]\s*(1000|2000|5000|9999)\b|fetchAll|getAllData|allData/i.test(source)) {
168
+ result.warnings.push({
169
+ code: 'unbounded-query-risk',
170
+ message: '发现疑似大页或全量拉取;列表、选择器和报表应使用服务端分页、搜索字段或 data-view',
171
+ file: relativeFile,
172
+ });
173
+ }
174
+ if (/(localStorage|sessionStorage).{0,80}(role|permission)|(role|permission).{0,80}(localStorage|sessionStorage)/i.test(source)) {
175
+ result.warnings.push({
176
+ code: 'frontend-permission-risk',
177
+ message: '发现疑似前端存储权限判断;权限和数据范围必须以后端权限组、public grants 或接口返回为准',
178
+ file: relativeFile,
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ function listSourceFiles(cwd) {
185
+ const files = [];
186
+ for (const relativeDir of SOURCE_DIRS) {
187
+ const dir = path.join(cwd, relativeDir);
188
+ if (fs.existsSync(dir)) collectSourceFiles(dir, files);
189
+ }
190
+ return files;
191
+ }
192
+
193
+ function collectSourceFiles(dir, files) {
194
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
195
+ if (SKIP_DIRS.has(entry.name)) continue;
196
+ const filePath = path.join(dir, entry.name);
197
+ if (entry.isDirectory()) {
198
+ collectSourceFiles(filePath, files);
199
+ continue;
200
+ }
201
+ if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
202
+ files.push(filePath);
203
+ }
204
+ }
205
+ }
206
+
207
+ function reviewAcceptanceArtifacts(result, cwd) {
208
+ const acceptanceDir = path.join(cwd, 'docs', 'acceptance');
209
+ const acceptanceFile = path.join(cwd, 'docs', 'acceptance.md');
210
+ if (fs.existsSync(acceptanceDir) || fs.existsSync(acceptanceFile)) return;
211
+ result.suggestions.push({
212
+ code: 'missing-acceptance-artifacts',
213
+ message: '建议补充 docs/acceptance/ 或 docs/acceptance.md,沉淀验收路径、账号、数据、发布 releaseId 和回滚方式',
214
+ });
215
+ }
216
+
217
+ module.exports = {
218
+ buildDesignReview,
219
+ renderDesignReview,
220
+ };
package/lib/http.js CHANGED
@@ -52,7 +52,10 @@ async function requestJson(baseUrl, apiPath, options = {}) {
52
52
 
53
53
  if (!response.ok) {
54
54
  const message = payload?.message || response.statusText || 'request failed';
55
- throw new Error(maskText(`HTTP ${response.status}: ${message}`));
55
+ const error = new Error(maskText(`HTTP ${response.status}: ${message}`));
56
+ error.status = response.status;
57
+ error.payload = payload;
58
+ throw error;
56
59
  }
57
60
 
58
61
  return payload;
@@ -6,6 +6,16 @@ const { getProfile, loadConfig, saveProjectState } = require('./config');
6
6
  const ROOT_DIR = path.join(__dirname, '..');
7
7
  const LEGACY_TEMPLATE_DIR = path.join(ROOT_DIR, 'templates', 'sy-lowcode-app-workspace');
8
8
  const REACT_SPA_TEMPLATE_DIR = path.join(ROOT_DIR, 'templates', 'openxiangda-react-spa');
9
+ const TEMPLATE_IGNORE_NAMES = new Set([
10
+ 'node_modules',
11
+ '.pnpm',
12
+ 'dist',
13
+ '.vite',
14
+ '.vite-temp',
15
+ '.cache',
16
+ 'coverage',
17
+ ]);
18
+ const TEMPLATE_IGNORE_PATHS = new Set(['.openxiangda/build-cache.json']);
9
19
 
10
20
  function initWorkspace(options = {}) {
11
21
  const targetDir = path.resolve(options.dir || process.cwd());
@@ -113,13 +123,14 @@ function normalizeRuntime(value) {
113
123
  return value === 'react-spa' || value === 'spa' ? 'react-spa' : 'legacy';
114
124
  }
115
125
 
116
- function copyTemplate(sourceDir, targetDir, replacements) {
126
+ function copyTemplate(sourceDir, targetDir, replacements, baseDir = sourceDir) {
117
127
  for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
118
128
  const sourcePath = path.join(sourceDir, entry.name);
129
+ if (shouldSkipTemplateEntry(baseDir, sourcePath, entry.name)) continue;
119
130
  const targetPath = path.join(targetDir, entry.name);
120
131
  if (entry.isDirectory()) {
121
132
  fs.mkdirSync(targetPath, { recursive: true });
122
- copyTemplate(sourcePath, targetPath, replacements);
133
+ copyTemplate(sourcePath, targetPath, replacements, baseDir);
123
134
  continue;
124
135
  }
125
136
  if (!entry.isFile()) continue;
@@ -129,6 +140,12 @@ function copyTemplate(sourceDir, targetDir, replacements) {
129
140
  }
130
141
  }
131
142
 
143
+ function shouldSkipTemplateEntry(baseDir, sourcePath, entryName) {
144
+ if (TEMPLATE_IGNORE_NAMES.has(entryName)) return true;
145
+ const relativePath = path.relative(baseDir, sourcePath).split(path.sep).join('/');
146
+ return TEMPLATE_IGNORE_PATHS.has(relativePath);
147
+ }
148
+
132
149
  function applyReplacements(content, replacements) {
133
150
  return Object.entries(replacements).reduce(
134
151
  (result, [key, value]) => result.split(key).join(value),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.104",
3
+ "version": "1.0.105",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -67,6 +67,7 @@
67
67
  "test:resource-plan": "node scripts/resource-plan-smoke.mjs",
68
68
  "test:design-gates": "node scripts/design-gates-smoke.mjs",
69
69
  "test:resource-cli": "node scripts/resource-cli-smoke.mjs",
70
+ "test:api-contract": "node scripts/openxiangda-api-contract-smoke.mjs",
70
71
  "test:help-no-side-effects": "node scripts/help-no-side-effects-smoke.mjs",
71
72
  "test:app-function-fallback": "node scripts/app-function-source-fallback-smoke.mjs",
72
73
  "test:runtime-deploy": "node scripts/runtime-deploy-smoke.mjs",
@@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest";
6
6
  describe("form runtime entry template", () => {
7
7
  it("injects AntD styles outside CSS layers with high priority", () => {
8
8
  const source = fs.readFileSync(
9
- path.resolve("packages/workspace-tools/scripts/build-forms.mjs"),
9
+ path.resolve("packages/sdk/src/build-source/scripts/build-forms.mjs"),
10
10
  "utf-8",
11
11
  );
12
12
 
@@ -0,0 +1,25 @@
1
+ ---
2
+ description: src/resources/** glob — OpenXiangda React SPA resource manifests
3
+ globs: src/resources/**/*
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # OpenXiangda Resource Manifests
8
+
9
+ You are editing engineering-managed resource manifests under `src/resources/`. Local files use logical codes; platform IDs stay in `.openxiangda/state.json`.
10
+
11
+ ## Commands
12
+
13
+ ```bash
14
+ openxiangda resource validate --profile <name>
15
+ openxiangda resource plan --profile <name>
16
+ openxiangda resource publish --profile <name>
17
+ openxiangda resource typegen --profile <name>
18
+ ```
19
+
20
+ ## Rules
21
+
22
+ - Public routes require matching `routes` and `public-access` manifests.
23
+ - Guest access to forms, dataViews, functions, and connectors requires explicit policy `grants`.
24
+ - Connector secrets and third-party credentials belong in the platform backend, never in manifests or page source.
25
+ - Formal changes should keep Git as the source of truth: edit manifests, validate, plan, then publish.
@@ -0,0 +1,33 @@
1
+ ---
2
+ description: OpenXiangda React SPA workspace strong constraints
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # OpenXiangda React SPA Rule
7
+
8
+ This is an OpenXiangda React SPA workspace. See [AGENTS.md](mdc:AGENTS.md) for full guidance.
9
+
10
+ ## Hard route
11
+
12
+ | User says | Command |
13
+ |---|---|
14
+ | publish resources | `openxiangda resource publish --profile <name>` |
15
+ | deploy frontend | `openxiangda runtime deploy --profile <name>` |
16
+ | full deploy | `pnpm deploy -- --profile <name>` |
17
+ | diff / plan | `openxiangda resource plan --profile <name>` |
18
+ | diagnose | `openxiangda doctor --profile <name> --json` |
19
+
20
+ ## Always
21
+
22
+ - Writes and deploys must pass `--profile <name>` explicitly.
23
+ - Architecture-class work is plan-gated: run `openxiangda design gates --topic <code> --json` and wait for confirmation.
24
+ - `src/resources/**` is the resource source of truth; use `validate -> plan -> publish`.
25
+ - React routes live in `src/app/router.tsx`; frontend artifacts are deployed with `openxiangda runtime deploy`.
26
+ - Backend permissions and public-access grants are authoritative.
27
+
28
+ ## Never
29
+
30
+ - Do not run `lowcode-workspace publish-all`, `pnpm publish:all`, or `pnpm openxiangda:publish` directly.
31
+ - Do not deploy without `--profile`.
32
+ - Do not use legacy `?publicAccess=guest`, `isRenderNav`, or workbench page parameters for new React SPA routes.
33
+ - Do not commit tokens, AK/SK, or third-party secrets.
@@ -0,0 +1,25 @@
1
+ ---
2
+ description: src/resources/** glob — OpenXiangda React SPA 资源 manifest 规则
3
+ glob: src/resources/**/*
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # OpenXiangda Resource Manifests
8
+
9
+ You are editing engineering-managed resource manifests under `src/resources/`. Local files use logical codes; platform IDs stay in `.openxiangda/state.json`.
10
+
11
+ ## Commands
12
+
13
+ ```bash
14
+ openxiangda resource validate --profile <name>
15
+ openxiangda resource plan --profile <name>
16
+ openxiangda resource publish --profile <name>
17
+ openxiangda resource typegen --profile <name>
18
+ ```
19
+
20
+ ## Rules
21
+
22
+ - Public routes require matching `routes` and `public-access` manifests.
23
+ - Guest access to forms, dataViews, functions, and connectors requires explicit policy `grants`.
24
+ - Connector secrets and third-party credentials belong in the platform backend, never in manifests or page source.
25
+ - Formal changes should keep Git as the source of truth: edit manifests, validate, plan, then publish.
@@ -0,0 +1,33 @@
1
+ ---
2
+ description: OpenXiangda React SPA 工作区强约束 — 路由、命令、不变量、禁令
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # OpenXiangda React SPA Rule
7
+
8
+ This is an OpenXiangda React SPA workspace. Read [AGENTS.md](AGENTS.md) for full guidance. New apps use `resource publish` plus `runtime deploy`; do not use classic workspace publish flows for frontend deployment.
9
+
10
+ ## Hard route
11
+
12
+ | 用户说 | 必用命令 |
13
+ |---|---|
14
+ | 发布资源 / publish resources | `openxiangda resource publish --profile <name>` |
15
+ | 发布前端 / deploy runtime | `openxiangda runtime deploy --profile <name>` |
16
+ | 完整部署 / deploy | `pnpm deploy -- --profile <name>` |
17
+ | 资源计划 / diff | `openxiangda resource plan --profile <name>` |
18
+ | 诊断 / 环境 | `openxiangda doctor --profile <name> --json` / `openxiangda env --profile <name>` |
19
+
20
+ ## Always
21
+
22
+ - 发布和写平台资源必须显式传 `--profile <name>`。
23
+ - 架构类需求先跑 `openxiangda design gates --topic <code> --json` 并等用户确认。
24
+ - `src/resources/**` 是工程化资源来源,正式多资源变更走 `validate -> plan -> publish`。
25
+ - React SPA 路由由 `src/app/router.tsx` 管理,前端包通过 `openxiangda runtime deploy` 发布。
26
+ - 页面权限、表单权限、公开访问 grants 以后端资源为准;前端只做展示保护。
27
+
28
+ ## Never
29
+
30
+ - 不要直接运行 `lowcode-workspace publish-all`、`pnpm publish:all` 或 `pnpm openxiangda:publish`。
31
+ - 不要省略 `--profile` 发布。
32
+ - 不要使用旧 `?publicAccess=guest`、`isRenderNav` 或 workbench 参数模型开发新 React SPA。
33
+ - 不要把 token、AK、SK、第三方密钥写入项目文件。
@@ -23,6 +23,7 @@ pnpm typecheck
23
23
  pnpm typecheck:js-code
24
24
  pnpm build
25
25
  pnpm build-js-code
26
+ pnpm deploy -- --profile <name>
26
27
  openxiangda workspace publish --profile <name> --form <formCode>
27
28
  openxiangda doctor --profile <name> --json
28
29
  openxiangda design gates --topic public-access --json
@@ -32,6 +33,8 @@ openxiangda runtime deploy --profile <name>
32
33
  openxiangda commands --json
33
34
  ```
34
35
 
36
+ `pnpm deploy -- --profile <name>` 会按顺序执行 `openxiangda resource publish --profile <name>` 与 `openxiangda runtime deploy --profile <name>`。不要直接运行 `pnpm publish:all`、`pnpm openxiangda:publish` 或 `lowcode-workspace publish-all`;这些是 legacy classic workspace 的内部发布入口,会被 `scripts/guard-publish.mjs` 拦截。
37
+
35
38
  完整发布顺序:
36
39
 
37
40
  ```bash
@@ -12,13 +12,16 @@
12
12
  "sync-schema": "lowcode-workspace sync-schema",
13
13
  "publish:all": "lowcode-workspace publish-all",
14
14
  "openxiangda:publish": "lowcode-workspace publish-all",
15
+ "prepublish:all": "pnpm _guard:publish",
16
+ "preopenxiangda:publish": "pnpm _guard:publish",
17
+ "_guard:publish": "node scripts/guard-publish.mjs",
15
18
  "typecheck": "tsc -p tsconfig.app.json --noEmit",
16
19
  "typecheck:js-code": "tsc -p tsconfig.js-code-nodes.json --noEmit",
17
20
  "check": "pnpm typecheck && pnpm build",
18
21
  "resources:plan": "openxiangda resource plan",
19
22
  "resources:publish": "openxiangda resource publish",
20
23
  "runtime:deploy": "openxiangda runtime deploy",
21
- "deploy": "openxiangda resource publish && openxiangda runtime deploy",
24
+ "deploy": "node scripts/deploy.mjs",
22
25
  "typegen": "openxiangda resource typegen"
23
26
  },
24
27
  "dependencies": {
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+
5
+ const args = process.argv.slice(2);
6
+ const profile = readProfile(args);
7
+
8
+ if (!profile) {
9
+ console.error('');
10
+ console.error('错误:React SPA 发布必须显式指定 profile。');
11
+ console.error('');
12
+ console.error('用法:');
13
+ console.error(' pnpm deploy -- --profile <name>');
14
+ console.error(' openxiangda resource publish --profile <name>');
15
+ console.error(' openxiangda runtime deploy --profile <name>');
16
+ console.error('');
17
+ process.exit(1);
18
+ }
19
+
20
+ run('openxiangda', ['resource', 'publish', '--profile', profile]);
21
+ run('openxiangda', ['runtime', 'deploy', '--profile', profile]);
22
+
23
+ function readProfile(argv) {
24
+ const profileIndex = argv.indexOf('--profile');
25
+ if (profileIndex >= 0) {
26
+ const value = argv[profileIndex + 1];
27
+ return value && !value.startsWith('-') ? value : null;
28
+ }
29
+ const inline = argv.find(arg => arg.startsWith('--profile='));
30
+ return inline ? inline.slice('--profile='.length) || null : null;
31
+ }
32
+
33
+ function run(command, commandArgs) {
34
+ const result = spawnSync(command, commandArgs, {
35
+ stdio: 'inherit',
36
+ env: process.env,
37
+ shell: process.platform === 'win32',
38
+ });
39
+ if (result.status !== 0) {
40
+ process.exit(result.status ?? 1);
41
+ }
42
+ }
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+
3
+ const lines = [
4
+ '',
5
+ '错误:React SPA 工作区不要直接调用 lowcode-workspace publish-all。',
6
+ '',
7
+ 'React SPA 的发布入口分两步,且必须显式指定 profile:',
8
+ ' openxiangda resource publish --profile <name>',
9
+ ' openxiangda runtime deploy --profile <name>',
10
+ '',
11
+ '或者使用模板脚本:',
12
+ ' pnpm deploy -- --profile <name>',
13
+ '',
14
+ 'legacy lowcode-workspace publish-all 只用于旧 classic workspace 的兼容流程。',
15
+ '',
16
+ ];
17
+
18
+ for (const line of lines) {
19
+ console.error(line);
20
+ }
21
+ process.exit(1);