openxiangda 1.0.0

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.
Files changed (121) hide show
  1. package/README.md +58 -0
  2. package/bin/openxiangda.js +11 -0
  3. package/lib/cli.js +2423 -0
  4. package/lib/config.js +121 -0
  5. package/lib/http.js +47 -0
  6. package/lib/skills.js +371 -0
  7. package/lib/utils.js +87 -0
  8. package/lib/workspace-init.js +139 -0
  9. package/openxiangda-skills/SKILL.md +128 -0
  10. package/openxiangda-skills/references/architecture-patterns.md +242 -0
  11. package/openxiangda-skills/references/automation-v3.md +129 -0
  12. package/openxiangda-skills/references/component-guide.md +198 -0
  13. package/openxiangda-skills/references/forms/component-registry.md +53 -0
  14. package/openxiangda-skills/references/forms/form-schema.md +109 -0
  15. package/openxiangda-skills/references/forms/layout-and-rules.md +24 -0
  16. package/openxiangda-skills/references/openxiangda-api.md +466 -0
  17. package/openxiangda-skills/references/pages/page-sdk.md +13 -0
  18. package/openxiangda-skills/references/pages/publish-flow.md +36 -0
  19. package/openxiangda-skills/references/pages/workspace-structure.md +38 -0
  20. package/openxiangda-skills/references/permissions-settings.md +147 -0
  21. package/openxiangda-skills/references/platform-data-model.md +305 -0
  22. package/openxiangda-skills/references/style-system.md +492 -0
  23. package/openxiangda-skills/references/troubleshooting.md +246 -0
  24. package/openxiangda-skills/references/workflow-v3.md +105 -0
  25. package/openxiangda-skills/references/workspace-state.md +45 -0
  26. package/openxiangda-skills/skills/openxiangda-app/SKILL.md +64 -0
  27. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +143 -0
  28. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +76 -0
  29. package/openxiangda-skills/skills/openxiangda-inspect/SKILL.md +40 -0
  30. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +62 -0
  31. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +95 -0
  32. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +97 -0
  33. package/package.json +126 -0
  34. package/packages/sdk/bin/lowcode-workspace.mjs +4 -0
  35. package/packages/sdk/dist/build/index.cjs +33 -0
  36. package/packages/sdk/dist/build/index.cjs.map +1 -0
  37. package/packages/sdk/dist/build/index.d.mts +40 -0
  38. package/packages/sdk/dist/build/index.d.ts +40 -0
  39. package/packages/sdk/dist/build/index.mjs +8 -0
  40. package/packages/sdk/dist/build/index.mjs.map +1 -0
  41. package/packages/sdk/dist/components/index.cjs +18700 -0
  42. package/packages/sdk/dist/components/index.cjs.map +1 -0
  43. package/packages/sdk/dist/components/index.d.mts +2094 -0
  44. package/packages/sdk/dist/components/index.d.ts +2094 -0
  45. package/packages/sdk/dist/components/index.mjs +18649 -0
  46. package/packages/sdk/dist/components/index.mjs.map +1 -0
  47. package/packages/sdk/dist/runtime/index.cjs +1469 -0
  48. package/packages/sdk/dist/runtime/index.cjs.map +1 -0
  49. package/packages/sdk/dist/runtime/index.d.mts +831 -0
  50. package/packages/sdk/dist/runtime/index.d.ts +831 -0
  51. package/packages/sdk/dist/runtime/index.mjs +1420 -0
  52. package/packages/sdk/dist/runtime/index.mjs.map +1 -0
  53. package/packages/sdk/dist/styles/antd-theme.cjs +60 -0
  54. package/packages/sdk/dist/styles/antd-theme.cjs.map +1 -0
  55. package/packages/sdk/dist/styles/antd-theme.d.mts +5 -0
  56. package/packages/sdk/dist/styles/antd-theme.d.ts +5 -0
  57. package/packages/sdk/dist/styles/antd-theme.mjs +35 -0
  58. package/packages/sdk/dist/styles/antd-theme.mjs.map +1 -0
  59. package/packages/sdk/dist/styles/tailwind-preset.cjs +2641 -0
  60. package/packages/sdk/dist/styles/tailwind-preset.cjs.map +1 -0
  61. package/packages/sdk/dist/styles/tailwind-preset.d.mts +75 -0
  62. package/packages/sdk/dist/styles/tailwind-preset.d.ts +75 -0
  63. package/packages/sdk/dist/styles/tailwind-preset.mjs +2618 -0
  64. package/packages/sdk/dist/styles/tailwind-preset.mjs.map +1 -0
  65. package/packages/sdk/dist/styles/tokens.css +73 -0
  66. package/packages/sdk/src/build-source/README.md +9 -0
  67. package/packages/sdk/src/build-source/bin/lowcode-workspace.mjs +7 -0
  68. package/packages/sdk/src/build-source/package.json +34 -0
  69. package/packages/sdk/src/build-source/scripts/build-forms.mjs +824 -0
  70. package/packages/sdk/src/build-source/scripts/build-forms.runtime-entry.test.ts +18 -0
  71. package/packages/sdk/src/build-source/scripts/build-pages.mjs +793 -0
  72. package/packages/sdk/src/build-source/scripts/build-workspace.mjs +64 -0
  73. package/packages/sdk/src/build-source/scripts/publish-all.mjs +127 -0
  74. package/packages/sdk/src/build-source/scripts/publish-oss.mjs +149 -0
  75. package/packages/sdk/src/build-source/scripts/register-bundle.mjs +1 -0
  76. package/packages/sdk/src/build-source/scripts/register.mjs +329 -0
  77. package/packages/sdk/src/build-source/scripts/sync-schema.mjs +301 -0
  78. package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +639 -0
  79. package/packages/sdk/src/build-source/scripts/utils/form-api.test.ts +244 -0
  80. package/packages/sdk/src/build-source/scripts/utils/form-runtime-assets.mjs +57 -0
  81. package/packages/sdk/src/build-source/scripts/utils/form-runtime-assets.test.ts +135 -0
  82. package/packages/sdk/src/build-source/scripts/utils/incremental.mjs +210 -0
  83. package/packages/sdk/src/build-source/scripts/utils/load-config.mjs +257 -0
  84. package/packages/sdk/src/build-source/scripts/utils/load-config.test.ts +44 -0
  85. package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +70 -0
  86. package/packages/sdk/src/build-source/scripts/utils/namespace-css.mjs +61 -0
  87. package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +128 -0
  88. package/packages/sdk/src/build-source/scripts/utils/pages.mjs +80 -0
  89. package/packages/sdk/src/build-source/scripts/utils/progress.mjs +57 -0
  90. package/packages/sdk/src/build-source/scripts/utils/register-payload.mjs +89 -0
  91. package/packages/sdk/src/build-source/scripts/utils/register-payload.test.ts +76 -0
  92. package/packages/sdk/src/build-source/scripts/utils/runtime-css-check.mjs +44 -0
  93. package/packages/sdk/src/build-source/scripts/utils/runtime-css-check.test.ts +54 -0
  94. package/packages/sdk/src/build-source/scripts/utils/schema-transform.mjs +130 -0
  95. package/packages/sdk/src/build-source/scripts/utils/schema-transform.test.ts +141 -0
  96. package/packages/sdk/src/build-source/scripts/utils/tailwind-config.mjs +227 -0
  97. package/packages/sdk/src/build-source/scripts/utils/tailwind-config.test.ts +187 -0
  98. package/packages/sdk/src/build-source/src/cli.mjs +679 -0
  99. package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +34 -0
  100. package/templates/sy-lowcode-app-workspace/examples/forms/customer/page.tsx +1 -0
  101. package/templates/sy-lowcode-app-workspace/examples/forms/customer/schema.ts +35 -0
  102. package/templates/sy-lowcode-app-workspace/index.html +12 -0
  103. package/templates/sy-lowcode-app-workspace/package.json +49 -0
  104. package/templates/sy-lowcode-app-workspace/postcss.config.cjs +6 -0
  105. package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +100 -0
  106. package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +26 -0
  107. package/templates/sy-lowcode-app-workspace/src/forms/.gitkeep +1 -0
  108. package/templates/sy-lowcode-app-workspace/src/forms/README.md +48 -0
  109. package/templates/sy-lowcode-app-workspace/src/index.css +28 -0
  110. package/templates/sy-lowcode-app-workspace/src/js-code-nodes/.gitkeep +1 -0
  111. package/templates/sy-lowcode-app-workspace/src/js-code-nodes/types.d.ts +3 -0
  112. package/templates/sy-lowcode-app-workspace/src/main.tsx +36 -0
  113. package/templates/sy-lowcode-app-workspace/src/pages/.gitkeep +1 -0
  114. package/templates/sy-lowcode-app-workspace/src/shared/form-schema.ts +128 -0
  115. package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +31 -0
  116. package/templates/sy-lowcode-app-workspace/tailwind.config.cjs +30 -0
  117. package/templates/sy-lowcode-app-workspace/tsconfig.app.json +24 -0
  118. package/templates/sy-lowcode-app-workspace/tsconfig.js-code-nodes.json +15 -0
  119. package/templates/sy-lowcode-app-workspace/tsconfig.json +7 -0
  120. package/templates/sy-lowcode-app-workspace/tsconfig.node.json +10 -0
  121. package/templates/sy-lowcode-app-workspace/vite.config.ts +32 -0
@@ -0,0 +1,89 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ getPageAssetBaseUrl,
5
+ getPageAssetUrl,
6
+ getPageRuntimeUrl,
7
+ rootDir,
8
+ } from "./load-config.mjs";
9
+
10
+ function readPageRuntimeManifest() {
11
+ const manifestPath = path.resolve(rootDir, "dist/page-runtime/manifest.json");
12
+ if (!fs.existsSync(manifestPath)) return null;
13
+ try {
14
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
15
+ if (
16
+ manifest?.protocol !== "sy-page-runtime" ||
17
+ manifest?.majorVersion !== 1 ||
18
+ !manifest?.version ||
19
+ !manifest?.files?.entry
20
+ ) {
21
+ return null;
22
+ }
23
+ return manifest;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ export function buildPageMenuConfig(config, pageConfig) {
30
+ if (pageConfig.menu?.enabled === false) {
31
+ return { enabled: false };
32
+ }
33
+
34
+ return {
35
+ enabled: true,
36
+ name: pageConfig.menu?.name || pageConfig.name,
37
+ parentId:
38
+ pageConfig.menu && "parentId" in pageConfig.menu
39
+ ? pageConfig.menu.parentId
40
+ : config.defaults.pageMenuParentId || null,
41
+ icon:
42
+ pageConfig.menu && "icon" in pageConfig.menu
43
+ ? pageConfig.menu.icon
44
+ : config.defaults.pageMenuIcon || null,
45
+ };
46
+ }
47
+
48
+ export function buildDirectPagePublishPayload(config, pages) {
49
+ const pageRuntime = readPageRuntimeManifest();
50
+ return {
51
+ appType: config.appType,
52
+ userId: config.userId,
53
+ version: config.version,
54
+ buildId: config.buildId,
55
+ pages: pages.map((page) => {
56
+ const pageBaseUrl = getPageAssetBaseUrl(config, page.config.code);
57
+ const pageEntryUrl = getPageAssetUrl(config, page.config.code, "index.js");
58
+ const pageStyleUrl = getPageAssetUrl(config, page.config.code, "style.css");
59
+ const runtimeJsUrl = pageRuntime?.files?.entry
60
+ ? getPageRuntimeUrl(config, pageRuntime.files.entry)
61
+ : null;
62
+ const runtimeCssUrl = pageRuntime?.files?.css
63
+ ? getPageRuntimeUrl(config, pageRuntime.files.css)
64
+ : null;
65
+ return {
66
+ code: page.config.code,
67
+ name: page.config.name,
68
+ description: page.config.description || "",
69
+ route: page.config.route,
70
+ props: page.config.props || {},
71
+ dataSources: page.config.dataSources || [],
72
+ runtime: {
73
+ entryUrl: pageEntryUrl,
74
+ cssUrls: [runtimeCssUrl, pageStyleUrl].filter(Boolean),
75
+ jsUrls: runtimeJsUrl ? [runtimeJsUrl] : [pageEntryUrl],
76
+ cdnBaseUrl: pageBaseUrl,
77
+ framework: "react",
78
+ frameworkVersion: config.defaults.frameworkVersion,
79
+ protocolVersion: config.defaults.protocolVersion,
80
+ protocol: pageRuntime?.protocol || undefined,
81
+ version: config.version,
82
+ cssIsolation:
83
+ page.config.cssIsolation || config.defaults.cssIsolation,
84
+ },
85
+ menu: buildPageMenuConfig(config, page.config),
86
+ };
87
+ }),
88
+ };
89
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { getFormBundleUrl, getPageAssetUrl } from "./load-config.mjs";
4
+ import { discoverPages } from "./pages.mjs";
5
+ import { buildDirectPagePublishPayload } from "./register-payload.mjs";
6
+
7
+ const config = {
8
+ appType: "APP_TEST",
9
+ userId: "user-1",
10
+ version: "1.2.3",
11
+ buildId: "BUILD_001",
12
+ oss: {
13
+ bucket: "bucket",
14
+ region: "oss-cn-hangzhou",
15
+ pathPrefix: "lowcode/app-workspace/dev",
16
+ },
17
+ defaults: {
18
+ frameworkVersion: "19.0.0",
19
+ protocolVersion: "1.0",
20
+ cssIsolation: "namespace",
21
+ pageMenuParentId: "MENU_PARENT",
22
+ pageMenuIcon: "dashboard",
23
+ },
24
+ };
25
+
26
+ describe("register payload helpers", () => {
27
+ it("discovers publishable code pages", async () => {
28
+ const pages = await discoverPages("customer-dashboard");
29
+
30
+ expect(pages).toHaveLength(1);
31
+ expect(pages[0].config.code).toBe("customer-dashboard");
32
+ });
33
+
34
+ it("builds stable form and page asset URLs from one build id", () => {
35
+ expect(getFormBundleUrl(config, "customer-info", "index.js")).toBe(
36
+ "https://bucket.oss-cn-hangzhou.aliyuncs.com/lowcode/app-workspace/dev/1.2.3/BUILD_001/forms/customer-info/index.js",
37
+ );
38
+ expect(getPageAssetUrl(config, "customer-dashboard", "style.css")).toBe(
39
+ "https://bucket.oss-cn-hangzhou.aliyuncs.com/lowcode/app-workspace/dev/1.2.3/BUILD_001/pages/customer-dashboard/style.css",
40
+ );
41
+ });
42
+
43
+ it("builds direct custom page payloads without manifest fields", () => {
44
+ const payload = buildDirectPagePublishPayload(config, [
45
+ {
46
+ config: {
47
+ code: "customer-dashboard",
48
+ name: "客户经营看板",
49
+ route: { pathKey: "customer-dashboard" },
50
+ props: { title: "客户经营看板" },
51
+ dataSources: [],
52
+ },
53
+ },
54
+ ]);
55
+
56
+ expect(payload.pages[0].runtime.entryUrl).toBe(
57
+ "https://bucket.oss-cn-hangzhou.aliyuncs.com/lowcode/app-workspace/dev/1.2.3/BUILD_001/pages/customer-dashboard/index.js",
58
+ );
59
+ expect(JSON.stringify(payload)).not.toMatch(/manifest/i);
60
+ });
61
+
62
+ it("marks menu disabled pages so the backend skips menu upsert", () => {
63
+ const payload = buildDirectPagePublishPayload(config, [
64
+ {
65
+ config: {
66
+ code: "internal-dashboard",
67
+ name: "内部看板",
68
+ route: { pathKey: "internal-dashboard" },
69
+ menu: { enabled: false },
70
+ },
71
+ },
72
+ ]);
73
+
74
+ expect(payload.pages[0].menu).toEqual({ enabled: false });
75
+ });
76
+ });
@@ -0,0 +1,44 @@
1
+ import fs from "node:fs";
2
+
3
+ export const requiredRuntimeCssSelectors = [
4
+ ".flex",
5
+ ".grid",
6
+ ".rounded-lg",
7
+ ".bg-ant-bg-layout",
8
+ ".sy-form-layout",
9
+ ".sy-layout-field",
10
+ ".sy-field-wrapper",
11
+ ".sy-field-control",
12
+ ".sy-subform",
13
+ ];
14
+
15
+ function selectorPattern(selector) {
16
+ const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17
+ return new RegExp(`(^|[^A-Za-z0-9_-])${escaped}([^A-Za-z0-9_-]|$)`);
18
+ }
19
+
20
+ export function getMissingRuntimeCssSelectors(
21
+ css,
22
+ selectors = requiredRuntimeCssSelectors,
23
+ ) {
24
+ return selectors.filter((selector) => !selectorPattern(selector).test(css));
25
+ }
26
+
27
+ export function validateRuntimeCssFile(
28
+ cssPath,
29
+ { label = "runtime", selectors = requiredRuntimeCssSelectors } = {},
30
+ ) {
31
+ if (!cssPath || !fs.existsSync(cssPath) || fs.statSync(cssPath).size === 0) {
32
+ throw new Error(
33
+ `${label} style.css is empty or missing; run lowcode-workspace update to refresh Tailwind config and ensure src/index.css keeps Tailwind base in its own layer`,
34
+ );
35
+ }
36
+
37
+ const css = fs.readFileSync(cssPath, "utf-8");
38
+ const missing = getMissingRuntimeCssSelectors(css, selectors);
39
+ if (missing.length) {
40
+ throw new Error(
41
+ `${label} style.css is missing required runtime utility selectors: ${missing.join(", ")}; run lowcode-workspace update to refresh Tailwind preset/content scanning, then rebuild without stale runtime cache`,
42
+ );
43
+ }
44
+ }
@@ -0,0 +1,54 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+
7
+ import {
8
+ getMissingRuntimeCssSelectors,
9
+ validateRuntimeCssFile,
10
+ } from "./runtime-css-check.mjs";
11
+
12
+ let tempDirs: string[] = [];
13
+
14
+ function createTempDir() {
15
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "runtime-css-check-"));
16
+ tempDirs.push(dir);
17
+ return dir;
18
+ }
19
+
20
+ afterEach(() => {
21
+ for (const dir of tempDirs) {
22
+ fs.rmSync(dir, { recursive: true, force: true });
23
+ }
24
+ tempDirs = [];
25
+ });
26
+
27
+ describe("runtime CSS checks", () => {
28
+ it("accepts runtime CSS with required lowcode layout selectors", () => {
29
+ const dir = createTempDir();
30
+ const cssPath = path.join(dir, "style.css");
31
+ fs.writeFileSync(
32
+ cssPath,
33
+ ".flex{display:flex}.grid{display:grid}.rounded-lg{border-radius:.5rem}.bg-ant-bg-layout{background:var(--ant-color-bg-layout)}.sy-form-layout{display:flex}.sy-layout-field{width:100%}.sy-field-wrapper{display:flex}.sy-field-control .ant-input{width:100%}.sy-subform{display:flex}",
34
+ "utf-8",
35
+ );
36
+
37
+ expect(() => validateRuntimeCssFile(cssPath)).not.toThrow();
38
+ expect(
39
+ getMissingRuntimeCssSelectors(fs.readFileSync(cssPath, "utf-8")),
40
+ ).toEqual([]);
41
+ });
42
+
43
+ it("fails when Tailwind runtime utility selectors are missing", () => {
44
+ const dir = createTempDir();
45
+ const cssPath = path.join(dir, "style.css");
46
+ fs.writeFileSync(cssPath, ".ant-btn{display:inline-flex}", "utf-8");
47
+
48
+ expect(() =>
49
+ validateRuntimeCssFile(cssPath, { label: "代码页共享 runtime" }),
50
+ ).toThrow(
51
+ /代码页共享 runtime style\.css is missing required runtime utility selectors/,
52
+ );
53
+ });
54
+ });
@@ -0,0 +1,130 @@
1
+ export function normalizeComponentName(componentName) {
2
+ if (componentName === "TextAreaField") return "TextareaField";
3
+ return componentName;
4
+ }
5
+
6
+ export function getObjectSize(value) {
7
+ if (!value || typeof value !== "object" || Array.isArray(value)) return 0;
8
+ return Object.keys(value).length;
9
+ }
10
+
11
+ export function isValidTableName(value) {
12
+ const normalized = String(value || "").trim();
13
+ return Boolean(normalized) && normalized.toLowerCase() !== "null";
14
+ }
15
+
16
+ export function validateFormSchema(schema, formName = "unknown") {
17
+ if (!schema || typeof schema !== "object") {
18
+ throw new Error(`${formName}: schema 必须是对象`);
19
+ }
20
+ if (!schema.formMeta || typeof schema.formMeta !== "object") {
21
+ throw new Error(`${formName}: schema.formMeta 缺失`);
22
+ }
23
+ if (!Array.isArray(schema.fields) || schema.fields.length === 0) {
24
+ throw new Error(`${formName}: schema.fields 不能为空`);
25
+ }
26
+
27
+ const seenFieldIds = new Set();
28
+ schema.fields.forEach((field, index) => {
29
+ const fieldLabel = `${formName}: fields[${index}]`;
30
+ if (!field || typeof field !== "object") {
31
+ throw new Error(`${fieldLabel} 必须是对象`);
32
+ }
33
+ const fieldId = String(field.fieldId || "").trim();
34
+ if (!fieldId) {
35
+ throw new Error(`${fieldLabel}.fieldId 不能为空`);
36
+ }
37
+ if (seenFieldIds.has(fieldId)) {
38
+ throw new Error(`${formName}: fieldId 重复: ${fieldId}`);
39
+ }
40
+ seenFieldIds.add(fieldId);
41
+ if (!String(field.componentName || "").trim()) {
42
+ throw new Error(`${fieldLabel}.componentName 不能为空`);
43
+ }
44
+ });
45
+ }
46
+
47
+ export function createFormComponentNode(field, index) {
48
+ const componentName = normalizeComponentName(field.componentName);
49
+
50
+ return {
51
+ componentName,
52
+ id: field.id || `${field.fieldId || componentName}_${index + 1}`,
53
+ title: field.label || field.fieldId || componentName,
54
+ hidden: false,
55
+ isLocked: false,
56
+ condition: true,
57
+ conditionGroup: "",
58
+ props: {
59
+ ...field,
60
+ componentName,
61
+ isFormComponent: true,
62
+ fieldId: field.fieldId,
63
+ label: field.label || field.fieldId,
64
+ tips: field.tips || "",
65
+ value: field.value || "",
66
+ placeholder: field.placeholder || "",
67
+ },
68
+ };
69
+ }
70
+
71
+ export function transformToApiFormat(schema, formName = "unknown") {
72
+ validateFormSchema(schema, formName);
73
+
74
+ const { formMeta, fields } = schema;
75
+ const componentNodes = fields.map(createFormComponentNode);
76
+ const pageSchema = {
77
+ version: "2.0",
78
+ componentsTree: [
79
+ {
80
+ componentName: "Page",
81
+ id: `${formMeta.formUuid || "form"}_page`,
82
+ props: {},
83
+ children: componentNodes,
84
+ },
85
+ ],
86
+ };
87
+
88
+ return {
89
+ formUuid: formMeta.formUuid,
90
+ appType: formMeta.appType,
91
+ fieldCount: componentNodes.length,
92
+ schema: JSON.stringify(pageSchema),
93
+ packages: JSON.stringify({}),
94
+ };
95
+ }
96
+
97
+ export function assertSchemaSyncResult(body, expectedFieldCount) {
98
+ if (body?.code !== 200) {
99
+ throw new Error(
100
+ `API 业务失败: ${body?.code || "unknown"} ${body?.message || ""}`,
101
+ );
102
+ }
103
+
104
+ if (!isValidTableName(body?.data?.tableName)) {
105
+ throw new Error("API 同步后未返回有效 tableName,表结构可能未创建");
106
+ }
107
+
108
+ const actualFieldCount = getObjectSize(body?.data?.formFields);
109
+ if (actualFieldCount !== expectedFieldCount) {
110
+ throw new Error(
111
+ `API 同步后 formFields 数量不匹配,期望 ${expectedFieldCount},实际 ${actualFieldCount}`,
112
+ );
113
+ }
114
+ }
115
+
116
+ export function assertFormReadyForBundle(formMeta, formName = "unknown") {
117
+ if (!formMeta) {
118
+ throw new Error(`${formName}: 平台未找到表单定义`);
119
+ }
120
+ if (!isValidTableName(formMeta.tableName)) {
121
+ throw new Error(
122
+ `${formName}: 平台表单缺少 tableName,请先运行 pnpm sync-schema --form ${formName}`,
123
+ );
124
+ }
125
+ if (getObjectSize(formMeta.formFields) === 0) {
126
+ throw new Error(
127
+ `${formName}: 平台表单缺少 formFields,请先运行 pnpm sync-schema --form ${formName}`,
128
+ );
129
+ }
130
+ }
@@ -0,0 +1,141 @@
1
+ // @ts-nocheck
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import {
5
+ assertFormReadyForBundle,
6
+ assertSchemaSyncResult,
7
+ transformToApiFormat,
8
+ } from "./schema-transform.mjs";
9
+
10
+ describe("schema-transform", () => {
11
+ it("builds form component nodes with stable field metadata", () => {
12
+ const payload = transformToApiFormat(
13
+ {
14
+ formMeta: {
15
+ formUuid: "FORM_1",
16
+ appType: "APP_1",
17
+ title: "客户信息登记",
18
+ },
19
+ fields: [
20
+ {
21
+ fieldId: "customer_name",
22
+ componentName: "TextField",
23
+ label: "客户名称",
24
+ },
25
+ {
26
+ fieldId: "remark",
27
+ componentName: "TextAreaField",
28
+ label: "备注",
29
+ },
30
+ ],
31
+ },
32
+ "customer-info",
33
+ );
34
+
35
+ const schema = JSON.parse(payload.schema);
36
+ const nodes = schema.componentsTree[0].children;
37
+
38
+ expect(payload.fieldCount).toBe(2);
39
+ expect(nodes[0].props).toMatchObject({
40
+ fieldId: "customer_name",
41
+ componentName: "TextField",
42
+ isFormComponent: true,
43
+ });
44
+ expect(nodes[1].props).toMatchObject({
45
+ fieldId: "remark",
46
+ componentName: "TextareaField",
47
+ isFormComponent: true,
48
+ });
49
+ });
50
+
51
+ it("rejects duplicate field ids and empty fields", () => {
52
+ expect(() =>
53
+ transformToApiFormat(
54
+ {
55
+ formMeta: { formUuid: "FORM_1", appType: "APP_1", title: "表单" },
56
+ fields: [],
57
+ },
58
+ "empty-form",
59
+ ),
60
+ ).toThrow("schema.fields 不能为空");
61
+
62
+ expect(() =>
63
+ transformToApiFormat(
64
+ {
65
+ formMeta: { formUuid: "FORM_1", appType: "APP_1", title: "表单" },
66
+ fields: [
67
+ { fieldId: "name", componentName: "TextField", label: "姓名" },
68
+ { fieldId: "name", componentName: "TextField", label: "姓名2" },
69
+ ],
70
+ },
71
+ "duplicate-form",
72
+ ),
73
+ ).toThrow("fieldId 重复: name");
74
+ });
75
+
76
+ it("validates schema sync response before treating publish as ready", () => {
77
+ expect(() =>
78
+ assertSchemaSyncResult(
79
+ {
80
+ code: 200,
81
+ data: {
82
+ tableName: "form_app_1",
83
+ formFields: {
84
+ customer_name: {},
85
+ remark: {},
86
+ },
87
+ },
88
+ },
89
+ 2,
90
+ ),
91
+ ).not.toThrow();
92
+
93
+ expect(() =>
94
+ assertSchemaSyncResult(
95
+ {
96
+ code: 200,
97
+ data: {
98
+ tableName: "null",
99
+ formFields: { customer_name: {} },
100
+ },
101
+ },
102
+ 1,
103
+ ),
104
+ ).toThrow("有效 tableName");
105
+
106
+ expect(() =>
107
+ assertSchemaSyncResult(
108
+ {
109
+ code: 200,
110
+ data: {
111
+ tableName: "form_app_1",
112
+ formFields: { customer_name: {} },
113
+ },
114
+ },
115
+ 2,
116
+ ),
117
+ ).toThrow("formFields 数量不匹配");
118
+ });
119
+
120
+ it("rejects bundle registration before backend schema is initialized", () => {
121
+ expect(() =>
122
+ assertFormReadyForBundle(
123
+ {
124
+ tableName: "",
125
+ formFields: { customer_name: {} },
126
+ },
127
+ "customer-info",
128
+ ),
129
+ ).toThrow("缺少 tableName");
130
+
131
+ expect(() =>
132
+ assertFormReadyForBundle(
133
+ {
134
+ tableName: "form_app_1",
135
+ formFields: {},
136
+ },
137
+ "customer-info",
138
+ ),
139
+ ).toThrow("缺少 formFields");
140
+ });
141
+ });