openxiangda 1.0.270 → 1.0.271
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/application-environments.js +17 -0
- package/lib/cli.js +343 -78
- package/lib/release-plan.js +76 -63
- package/openxiangda-skills/SKILL.md +1 -1
- package/openxiangda-skills/references/notifications.md +28 -23
- package/openxiangda-skills/references/openxiangda-api.md +10 -30
- package/openxiangda-skills/references/pages/page-sdk.md +1 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +5 -7
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +2 -2
- package/package.json +22 -23
- package/packages/sdk/dist/runtime/index.cjs +39 -39
- package/packages/sdk/dist/runtime/index.mjs +39 -39
- package/packages/sdk/dist/runtime/react.cjs +39 -39
- package/packages/sdk/dist/runtime/react.d.mts +3 -0
- package/packages/sdk/dist/runtime/react.d.ts +3 -0
- package/packages/sdk/dist/runtime/react.mjs +46 -46
- package/packages/sdk/src/build-source/scripts/publish-all.mjs +33 -2
- package/packages/sdk/src/build-source/scripts/register.mjs +161 -1
- package/templates/openxiangda-react-spa/AGENTS.md +2 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +1 -1
|
@@ -99,6 +99,16 @@ const only = changedOnly
|
|
|
99
99
|
: targetPage
|
|
100
100
|
? [`pages/${targetPage}`]
|
|
101
101
|
: normalizeOnly(args.only);
|
|
102
|
+
const stageOnlyPageRelease = process.env.OPENXIANGDA_PAGE_STAGE_ONLY === "1";
|
|
103
|
+
if (
|
|
104
|
+
stageOnlyPageRelease &&
|
|
105
|
+
(only.length === 0 || only.some((target) => !target.startsWith("pages/")))
|
|
106
|
+
) {
|
|
107
|
+
console.error(
|
|
108
|
+
"❌ PAGE_RELEASE_STAGE_SCOPE_REQUIRED: stage-only 页面发布必须使用 --page 或只包含 pages/* 的精确 --only 范围",
|
|
109
|
+
);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
102
112
|
|
|
103
113
|
if (changedOnly && only.length === 0) {
|
|
104
114
|
console.log("✅ 没有检测到 src/forms 或 src/pages 的 git 变更,跳过发布");
|
|
@@ -145,7 +155,10 @@ const publishLegacyFormBundle =
|
|
|
145
155
|
Boolean(config.forms?.publishLegacyBundle);
|
|
146
156
|
|
|
147
157
|
const allModules = discoverWorkspaceModules();
|
|
148
|
-
const plan = planIncrementalPublish(allModules, {
|
|
158
|
+
const plan = planIncrementalPublish(allModules, {
|
|
159
|
+
force: force || stageOnlyPageRelease,
|
|
160
|
+
only,
|
|
161
|
+
});
|
|
149
162
|
printPlan(plan, "publish-all");
|
|
150
163
|
if (plan.changed.length === 0 && !force) {
|
|
151
164
|
console.log("✅ 没有检测到变更,跳过发布");
|
|
@@ -177,11 +190,29 @@ for (const moduleItem of plan.changed) {
|
|
|
177
190
|
} else if (moduleItem.kind === "pages") {
|
|
178
191
|
await run("build-pages.mjs", ["--page", moduleItem.name, ...maybeForce]);
|
|
179
192
|
await run("publish-oss.mjs", ["--page", moduleItem.name, ...maybeDryRun]);
|
|
180
|
-
|
|
193
|
+
if (!stageOnlyPageRelease) {
|
|
194
|
+
await run("register.mjs", ["--page", moduleItem.name, ...maybeDryRun]);
|
|
195
|
+
}
|
|
181
196
|
publishedModules.push(moduleItem);
|
|
182
197
|
}
|
|
183
198
|
}
|
|
184
199
|
|
|
200
|
+
if (stageOnlyPageRelease) {
|
|
201
|
+
const pageNames = publishedModules
|
|
202
|
+
.filter((moduleItem) => moduleItem.kind === "pages")
|
|
203
|
+
.map((moduleItem) => moduleItem.name);
|
|
204
|
+
if (pageNames.length === 0) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
"PAGE_RELEASE_STAGE_SCOPE_REQUIRED: stage-only 页面发布没有可登记的精确页面",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
await run("register.mjs", [
|
|
210
|
+
"--page-list-json",
|
|
211
|
+
JSON.stringify(pageNames),
|
|
212
|
+
...maybeDryRun,
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
215
|
+
|
|
185
216
|
if (!dryRun) {
|
|
186
217
|
commitIncrementalPublish(plan, publishedModules);
|
|
187
218
|
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
7
9
|
import { glob } from "glob";
|
|
8
10
|
import minimist from "minimist";
|
|
9
11
|
import {
|
|
@@ -31,6 +33,8 @@ const args = minimist(process.argv.slice(2).filter((arg) => arg !== "--"));
|
|
|
31
33
|
const dryRun = Boolean(args["dry-run"]);
|
|
32
34
|
const targetForm = args.form || null;
|
|
33
35
|
const targetPage = args.page || null;
|
|
36
|
+
const targetPageList = parsePageList(args["page-list-json"]);
|
|
37
|
+
const stageOnlyPageRelease = process.env.OPENXIANGDA_PAGE_STAGE_ONLY === "1";
|
|
34
38
|
const ensureExistingFormMenu =
|
|
35
39
|
Boolean(args["ensure-menu"]) ||
|
|
36
40
|
["1", "true", "yes"].includes(
|
|
@@ -48,12 +52,36 @@ register - 注册应用工作区产物到平台
|
|
|
48
52
|
--dry-run 只打印注册计划,不实际调用 API
|
|
49
53
|
--form <name> 只注册指定表单
|
|
50
54
|
--page <name> 只注册指定代码页目录
|
|
55
|
+
--page-list-json <JSON> 注册指定代码页目录数组(内部批量发布入口)
|
|
51
56
|
--ensure-menu 已存在 formUuid 的表单也尝试补齐菜单
|
|
52
57
|
--help, -h 显示帮助信息
|
|
53
58
|
`);
|
|
54
59
|
process.exit(0);
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
if (targetPage && targetPageList.length > 0) {
|
|
63
|
+
console.error("❌ --page 和 --page-list-json 不能同时使用");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parsePageList(value) {
|
|
68
|
+
if (value === undefined || value === null || value === "") return [];
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(String(value));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(`PAGE_REGISTER_SCOPE_INVALID: page-list-json 不是合法 JSON: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
76
|
+
throw new Error("PAGE_REGISTER_SCOPE_INVALID: page-list-json 必须是非空数组");
|
|
77
|
+
}
|
|
78
|
+
const normalized = parsed.map((item) => String(item || "").trim()).filter(Boolean);
|
|
79
|
+
if (normalized.length !== parsed.length || new Set(normalized).size !== normalized.length) {
|
|
80
|
+
throw new Error("PAGE_REGISTER_SCOPE_INVALID: page-list-json 包含空值或重复页面");
|
|
81
|
+
}
|
|
82
|
+
return normalized;
|
|
83
|
+
}
|
|
84
|
+
|
|
57
85
|
const config = await loadConfig();
|
|
58
86
|
const apiBase = getApiBaseUrl(config);
|
|
59
87
|
let accessToken = null;
|
|
@@ -251,7 +279,7 @@ function readFormRuntimeAssets(config) {
|
|
|
251
279
|
}
|
|
252
280
|
|
|
253
281
|
async function registerPages() {
|
|
254
|
-
const pages = await
|
|
282
|
+
const pages = await discoverSelectedPages();
|
|
255
283
|
if (pages.length === 0) {
|
|
256
284
|
return { succeeded: 0, failed: 0 };
|
|
257
285
|
}
|
|
@@ -265,6 +293,25 @@ async function registerPages() {
|
|
|
265
293
|
return { succeeded: pages.length, failed: 0 };
|
|
266
294
|
}
|
|
267
295
|
|
|
296
|
+
if (stageOnlyPageRelease) {
|
|
297
|
+
if (!isOpenXiangdaMode(config)) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
"PAGE_RELEASE_STAGE_CONTEXT_REQUIRED: stage-only 页面发布只支持 OpenXiangda 模式",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (!targetPage && targetPageList.length === 0) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
"PAGE_RELEASE_STAGE_SCOPE_REQUIRED: stage-only 页面发布必须提供精确页面范围",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const data = await publishStagedPagesWithCurrentCli(payload);
|
|
308
|
+
data.items?.forEach((item) => {
|
|
309
|
+
console.log(` ✓ 代码页 ${item.code} (${item.pageId})`);
|
|
310
|
+
});
|
|
311
|
+
writePagePublishResult(payload, { data }, false);
|
|
312
|
+
return { succeeded: pages.length, failed: 0 };
|
|
313
|
+
}
|
|
314
|
+
|
|
268
315
|
const response = await fetch(getPagePublishUrl(payload.appType), {
|
|
269
316
|
method: "POST",
|
|
270
317
|
headers: {
|
|
@@ -293,6 +340,119 @@ async function registerPages() {
|
|
|
293
340
|
return { succeeded: pages.length, failed: 0 };
|
|
294
341
|
}
|
|
295
342
|
|
|
343
|
+
async function discoverSelectedPages() {
|
|
344
|
+
if (targetPage) return discoverPages(targetPage);
|
|
345
|
+
const pages = await discoverPages("");
|
|
346
|
+
if (targetPageList.length === 0) return pages;
|
|
347
|
+
const byName = new Map();
|
|
348
|
+
for (const page of pages) {
|
|
349
|
+
byName.set(String(page.dirName || ""), page);
|
|
350
|
+
byName.set(String(page.config?.code || ""), page);
|
|
351
|
+
}
|
|
352
|
+
const missing = targetPageList.filter((name) => !byName.has(name));
|
|
353
|
+
if (missing.length > 0) {
|
|
354
|
+
throw new Error(`PAGE_REGISTER_SCOPE_INVALID: 找不到代码页 ${missing.join(",")}`);
|
|
355
|
+
}
|
|
356
|
+
return targetPageList.map((name) => byName.get(name));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function publishStagedPagesWithCurrentCli(payload) {
|
|
360
|
+
const cli = String(process.env.OPENXIANGDA_CLI || "").trim();
|
|
361
|
+
const profile = String(config.openXiangdaProfile || process.env.OPENXIANGDA_PROFILE || "").trim();
|
|
362
|
+
const changeId = String(process.env.OPENXIANGDA_CHANGE_ID || "").trim();
|
|
363
|
+
if (!cli || !profile || !changeId) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
"PAGE_RELEASE_STAGE_CONTEXT_REQUIRED: 缺少 OPENXIANGDA_CLI、OPENXIANGDA_PROFILE 或 OPENXIANGDA_CHANGE_ID",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openxiangda-page-stage-"));
|
|
370
|
+
const definitionsFile = path.join(tempDir, "pages.json");
|
|
371
|
+
try {
|
|
372
|
+
fs.writeFileSync(
|
|
373
|
+
definitionsFile,
|
|
374
|
+
`${JSON.stringify({ pages: payload.pages }, null, 2)}\n`,
|
|
375
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
376
|
+
);
|
|
377
|
+
const cliArgs = [
|
|
378
|
+
"page",
|
|
379
|
+
"publish",
|
|
380
|
+
"--pages-json",
|
|
381
|
+
definitionsFile,
|
|
382
|
+
"--version",
|
|
383
|
+
payload.version,
|
|
384
|
+
"--build-id",
|
|
385
|
+
payload.buildId,
|
|
386
|
+
"--profile",
|
|
387
|
+
profile,
|
|
388
|
+
"--change",
|
|
389
|
+
changeId,
|
|
390
|
+
"--json",
|
|
391
|
+
];
|
|
392
|
+
if (config.openXiangdaTarget) {
|
|
393
|
+
cliArgs.push("--environment", config.openXiangdaTarget);
|
|
394
|
+
}
|
|
395
|
+
const result = await runCurrentOpenXiangdaCli(cli, cliArgs);
|
|
396
|
+
const selectedPageCodes = Array.isArray(result?.stagedResource?.metadata?.selectedPageCodes)
|
|
397
|
+
? [...result.stagedResource.metadata.selectedPageCodes].sort()
|
|
398
|
+
: [];
|
|
399
|
+
const expectedPageCodes = payload.pages.map((page) => page.code).sort();
|
|
400
|
+
if (
|
|
401
|
+
result?.stagedResource?.kind !== "PageRelease" ||
|
|
402
|
+
selectedPageCodes.length !== expectedPageCodes.length ||
|
|
403
|
+
!expectedPageCodes.every((code, index) => code === selectedPageCodes[index])
|
|
404
|
+
) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`PAGE_RELEASE_STAGE_SCOPE_MISMATCH: staged PageRelease 范围不匹配;expected=${expectedPageCodes.join(",")} actual=${selectedPageCodes.join(",") || "-"}`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return result;
|
|
410
|
+
} finally {
|
|
411
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function runCurrentOpenXiangdaCli(cli, args) {
|
|
416
|
+
return new Promise((resolve, reject) => {
|
|
417
|
+
const command = cli.endsWith(".js") || cli.endsWith(".mjs") ? process.execPath : cli;
|
|
418
|
+
const commandArgs = command === process.execPath ? [cli, ...args] : args;
|
|
419
|
+
const child = spawn(command, commandArgs, {
|
|
420
|
+
cwd: rootDir,
|
|
421
|
+
env: process.env,
|
|
422
|
+
shell: false,
|
|
423
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
424
|
+
});
|
|
425
|
+
let stdout = "";
|
|
426
|
+
let stderr = "";
|
|
427
|
+
child.stdout.on("data", (chunk) => {
|
|
428
|
+
stdout += String(chunk);
|
|
429
|
+
});
|
|
430
|
+
child.stderr.on("data", (chunk) => {
|
|
431
|
+
stderr += String(chunk);
|
|
432
|
+
});
|
|
433
|
+
child.on("error", reject);
|
|
434
|
+
child.on("close", (code, signal) => {
|
|
435
|
+
if (signal || code !== 0) {
|
|
436
|
+
reject(
|
|
437
|
+
new Error(
|
|
438
|
+
`PAGE_RELEASE_STAGE_FAILED: 当前 OpenXiangda CLI 执行失败 (${signal || code}) ${stderr.trim() || stdout.trim()}`,
|
|
439
|
+
),
|
|
440
|
+
);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
resolve(JSON.parse(stdout));
|
|
445
|
+
} catch (error) {
|
|
446
|
+
reject(
|
|
447
|
+
new Error(
|
|
448
|
+
`PAGE_RELEASE_STAGE_RESULT_INVALID: CLI JSON 输出不可解析: ${error.message}`,
|
|
449
|
+
),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
296
456
|
function getPagePublishUrl(appType) {
|
|
297
457
|
if (isOpenXiangdaMode(config)) {
|
|
298
458
|
return `${apiBase}/openxiangda-api/v1/apps/${encodeURIComponent(appType)}/pages/publish`;
|
|
@@ -65,6 +65,8 @@ openxiangda commands --json
|
|
|
65
65
|
|
|
66
66
|
未登记环境的旧工作区只有在平台已核验“精确非删除目标来自多次历史发布、无法对应单一 Git 基线”时,才可在同一条 `release publish` 上增加 `--adopt-online-baseline --adoption-reason "..."`。CLI 只把该意图传给精确 `resource publish --only/--code` 阶段;表单 ensure、Runtime 与 App finalize 不接收,原因不足或没有精确资源阶段会在获取租约前失败关闭。
|
|
67
67
|
|
|
68
|
+
Legacy PageRelease 工作区的 `release publish` 会给 Page step 注入 `OPENXIANGDA_PAGE_STAGE_ONLY=1`,并要求当前包内 CLI 写入 staged PageRelease evidence;缺少证据时禁止 Root finalize。需要完整替换 Function/Automation manifest 时可成对增加 `--replace-manifest --reason "..."`,该权限只进入唯一的精确 Backend stage,不进入 Form、Page、Workflow 或 Root。
|
|
69
|
+
|
|
68
70
|
模板已停用无范围的 `pnpm deploy` 聚合入口。日常变更必须使用 `resource plan|publish <type> --only <codes>`(单资源可用 `--code <code>`)。Form bundle、Backend Release 和 Runtime 都先暂存;CLI 会按 `--change` 自动聚合 `.openxiangda/releases/<change>/staged-resources.json`,最后只由一次 Root App finalize 原子激活。Form Release 一旦 abort 绝不能作为幂等结果复用;重新执行相同精确表单发布时,CLI 会淘汰旧 staged 索引,平台会创建新的不可变 attempt,再由 Root App 一次事务重试。相同 change 的既有 staged FormRelease 只有在 CLI 重新核验服务端不可变状态、identity/hash、冻结 schema/formType、finalized 资源、parent/base revision 和当前 Form Head 后,才可重挂接到新的 baseline/session;`schemaSyncedAt` 只是本地缓存元数据。禁止伪造它、提前激活 Form 或用顺序激活多张表单绕过失败。不要使用 `workspace publish --form`、单独 `runtime activate`、`pnpm publish:all`、`pnpm openxiangda:publish` 或 `lowcode-workspace publish-all`。
|
|
69
71
|
|
|
70
72
|
同一发布同时包含 Form 设置和表单权限组时,必须由一条 `resource publish form-setting,form-permission-group` 命令暂存,并在 `--only` 中分别使用 `form-setting:<code>`、`form-permission-group:<code>`。SDD bundle 与 prepublish 校验使用同一精确契约;缺失、夹带、拆成两个 FormRelease 或提前激活都应失败关闭。
|
|
@@ -69,7 +69,7 @@ Delivery V2 自动从期望状态按资源指纹计算精确范围,使用 CLI
|
|
|
69
69
|
- ✅ Function/Automation 使用 Backend Release v2;正式多资源发布用精确 `--only/--code` 加 `--stage-only` 暂存,同一 child 可混合源码 create、无 `sourceFile` 的完整 v3 声明式 Automation manifest create、source-only update 和显式 manifest replacement,再由 Root App finalize 原子激活。声明式 create 自动选路;替换已有资源的整包 manifest 才需要另加 `--replace-manifest --reason "..."`。clone primary 与冻结仓库 ID 不同时,只有冻结 ID 已存在于 `repoAliases` 才会统一用于 Backend/Workflow/Root App Release;无交集继续失败关闭。
|
|
70
70
|
- ✅ 相同 change 已有 staged FormRelease 时,不要直发 schema、伪造 `schemaSyncedAt` 或提前激活 Form。CLI 只在重新核验服务端不可变状态、identity/hash、冻结 schema/formType、finalized 资源、parent/base revision 与当前 Form Head 后,才将 child 重挂接到新的 baseline/session;冲突继续失败关闭。
|
|
71
71
|
- ✅ 同一发布同时包含 Form 设置和表单权限组时,必须使用一条 `resource publish form-setting,form-permission-group`,并以 `form-setting:<code>`、`form-permission-group:<code>` 精确限定 `--only`;SDD bundle 与 prepublish 校验共享该契约,禁止缺失、夹带、拆分发布或提前激活。
|
|
72
|
-
- ✅ 未登记环境的旧工作区,正式 promotion 先聚合 mainline bundle 并 commit/push,再运行 `release publish --change <id> --profile <name
|
|
72
|
+
- ✅ 未登记环境的旧工作区,正式 promotion 先聚合 mainline bundle 并 commit/push,再运行 `release publish --change <id> --profile <name>`。Legacy Page step 必须遵守 CLI 注入的 `OPENXIANGDA_PAGE_STAGE_ONLY=1` 并通过当前包内 CLI 登记 staged PageRelease;缺少证据时 Root finalize 会失败关闭。平台核验精确非删除目标来自多次历史 lineage 后,可成对增加 `--adopt-online-baseline --adoption-reason "..."`;参数只进入精确 `resource publish --only/--code` 阶段。需要完整替换 Function/Automation manifest 时可成对增加 `--replace-manifest --reason "..."`,只进入唯一的精确 Backend stage。上述参数不进入 ensure、Page 或 App finalize,无兼容精确范围时在获取租约前失败关闭。
|
|
73
73
|
- ✅ 旧工作区已有 Root、且操作者明确授权无条件恢复时,可执行 `release app-activate <releaseId> --force-activate-without-validation --profile <name>`。该命令不读取 detail/capture,不要求 change、租约、baseline、源码 lineage、状态、parent、hash、resource head 或环境发布门禁;服务端直接在目标 tenant/appType 内以单事务切换 Root 与可识别的 staged children。
|
|
74
74
|
- ✅ 已通过 `environment init` 或 `environment attach` 接入的工作区使用 `release ship`,始终按 candidate → preproduction → production 执行。日常首条命令在预发停止,后续 `--confirm-production` 晋级;用户明确授权紧急发布时,首条命令可携带 `--confirm-production` 在一个命令内顺序完成两阶段,但不跳过预发、证据、CAS 或确认。candidate 封存源码、public、构建配置/脚本、稳定环境/资源绑定和两目标 Runtime 哈希产物;部署不现场重建,Backend/Runtime/Root 子发布始终使用 candidate sourceRevision,并将两条服务端 deployment 闭环为 `succeeded`。两套环境身份仍完全隔离,人工验收建议和 swap/policy/权限门禁保持不变。
|
|
75
75
|
- ✅ DataView `status` 仅是平台生命周期观察值;同一托管部署导致的 `active → draft → active` 不应让候选失效。`dataViewId`、`materializedViewName`、`storageMode`、其他资源状态、哈希、环境身份、CAS、租约和来源主线仍严格校验。
|