sy-lowcode-workspace-tools 0.1.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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * mime-types.mjs - MIME 类型映射
3
+ *
4
+ * 提供文件扩展名到 MIME 类型的映射,支持 JS/CSS/JSON/HTML、
5
+ * 图片、字体和其他常见静态资源类型。
6
+ */
7
+
8
+ import path from "node:path";
9
+
10
+ /** @type {Record<string, string>} 扩展名 → MIME 类型映射表 */
11
+ const MIME_MAP = {
12
+ // 脚本 & 样式
13
+ ".js": "application/javascript",
14
+ ".mjs": "application/javascript",
15
+ ".css": "text/css",
16
+ ".json": "application/json",
17
+ ".html": "text/html",
18
+ // 图片
19
+ ".png": "image/png",
20
+ ".jpg": "image/jpeg",
21
+ ".jpeg": "image/jpeg",
22
+ ".gif": "image/gif",
23
+ ".svg": "image/svg+xml",
24
+ ".webp": "image/webp",
25
+ ".ico": "image/x-icon",
26
+ ".avif": "image/avif",
27
+ // 字体
28
+ ".woff": "font/woff",
29
+ ".woff2": "font/woff2",
30
+ ".ttf": "font/ttf",
31
+ ".eot": "application/vnd.ms-fontobject",
32
+ // 其他
33
+ ".map": "application/json",
34
+ };
35
+
36
+ /**
37
+ * 根据文件路径获取对应的 MIME 类型
38
+ * @param {string} file - 文件路径
39
+ * @returns {string} MIME 类型
40
+ */
41
+ export function getContentType(file) {
42
+ const ext = path.extname(file).toLowerCase();
43
+ return MIME_MAP[ext] || "application/octet-stream";
44
+ }
45
+
46
+ /**
47
+ * 判断文件名是否包含 content hash(如 logo-abc123.png)
48
+ * 匹配模式:name-[8+位hex hash].ext
49
+ * @param {string} file - 文件名或路径
50
+ * @returns {boolean}
51
+ */
52
+ export function hasContentHash(file) {
53
+ const basename = path.basename(file);
54
+ return /\-[a-f0-9]{8,}\.[^.]+$/.test(basename);
55
+ }
56
+
57
+ /**
58
+ * 根据文件特征返回合适的 Cache-Control 头
59
+ * - 带 content hash 的资源 → 长期缓存 + immutable
60
+ * - 其他文件 → 长期缓存(依赖 buildId 路径做版本隔离)
61
+ * @param {string} file - 文件路径
62
+ * @returns {string} Cache-Control 头值
63
+ */
64
+ export function getCacheControl(file) {
65
+ if (hasContentHash(file)) {
66
+ return "public, max-age=31536000, immutable";
67
+ }
68
+ // 入口文件依赖 buildId 路径做版本隔离,同样可以长期缓存
69
+ return "public, max-age=31536000";
70
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * oss-client.mjs - 阿里云 OSS 客户端工具
3
+ *
4
+ * 封装 OSS 客户端初始化、带重试的上传、以及 CORS 规则确保。
5
+ */
6
+
7
+ import OSS from "ali-oss";
8
+
9
+ /**
10
+ * 创建阿里云 OSS 客户端实例
11
+ * @param {object} ossConfig - OSS 配置
12
+ * @param {string} ossConfig.region - OSS Region
13
+ * @param {string} ossConfig.bucket - Bucket 名称
14
+ * @param {string} ossConfig.accessKeyId - AccessKey ID
15
+ * @param {string} ossConfig.accessKeySecret - AccessKey Secret
16
+ * @returns {import("ali-oss")} OSS 客户端实例
17
+ */
18
+ export function createOSSClient(ossConfig) {
19
+ return new OSS({
20
+ region: ossConfig.region,
21
+ accessKeyId: ossConfig.accessKeyId,
22
+ accessKeySecret: ossConfig.accessKeySecret,
23
+ bucket: ossConfig.bucket,
24
+ });
25
+ }
26
+
27
+ /**
28
+ * 带重试机制的 OSS 上传
29
+ * @param {import("ali-oss")} client - OSS 客户端实例
30
+ * @param {string} ossKey - OSS 对象键
31
+ * @param {string} localPath - 本地文件路径
32
+ * @param {object} headers - HTTP 头(Cache-Control, Content-Type 等)
33
+ * @param {number} [retries=3] - 最大重试次数
34
+ * @returns {Promise<void>}
35
+ */
36
+ export async function uploadWithRetry(client, ossKey, localPath, headers, retries = 3) {
37
+ for (let i = 0; i < retries; i++) {
38
+ try {
39
+ await client.put(ossKey, localPath, { headers });
40
+ return;
41
+ } catch (err) {
42
+ if (i === retries - 1) throw err;
43
+ console.warn(` ⚠ 重试 ${i + 1}/${retries}: ${ossKey}`);
44
+ await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
45
+ }
46
+ }
47
+ }
48
+
49
+ /**
50
+ * @param {*} value
51
+ * @returns {string[]}
52
+ */
53
+ function toArray(value) {
54
+ if (Array.isArray(value)) return value;
55
+ if (value === undefined || value === null || value === "") return [];
56
+ return [value];
57
+ }
58
+
59
+ /**
60
+ * 检查单条 CORS 规则是否已满足指定 origins 和 methods
61
+ * @param {object} rule - CORS 规则对象
62
+ * @param {string[]} origins - 需要允许的来源
63
+ * @param {string[]} methods - 需要允许的方法
64
+ * @returns {boolean}
65
+ */
66
+ function corsRuleAllows(rule, origins, methods) {
67
+ const allowedOrigins = toArray(rule.allowedOrigin);
68
+ const allowedMethods = toArray(rule.allowedMethod).map((item) =>
69
+ String(item).toUpperCase(),
70
+ );
71
+
72
+ const originAllowed =
73
+ allowedOrigins.includes("*") ||
74
+ origins.every((origin) => allowedOrigins.includes(origin));
75
+ const methodAllowed = methods.every((method) =>
76
+ allowedMethods.includes(method),
77
+ );
78
+
79
+ return originAllowed && methodAllowed;
80
+ }
81
+
82
+ /**
83
+ * 确保 OSS Bucket 的 CORS 规则满足前端资源加载需求
84
+ * @param {import("ali-oss")} client - OSS 客户端实例
85
+ * @param {object} ossConfig - OSS 配置
86
+ * @param {string} ossConfig.bucket - Bucket 名称
87
+ * @param {boolean} [ossConfig.skipCors] - 是否跳过 CORS 检查
88
+ * @param {string[]} [ossConfig.corsOrigins] - 允许的来源列表
89
+ * @returns {Promise<void>}
90
+ */
91
+ export async function ensureBucketCors(client, ossConfig) {
92
+ if (ossConfig.skipCors) {
93
+ console.log(" CORS: 跳过(APP_OSS_SKIP_CORS=1)");
94
+ return;
95
+ }
96
+
97
+ const origins = ossConfig.corsOrigins?.length
98
+ ? ossConfig.corsOrigins
99
+ : ["*"];
100
+ const methods = ["GET", "HEAD"];
101
+ let rules = [];
102
+
103
+ try {
104
+ const result = await client.getBucketCORS(ossConfig.bucket);
105
+ rules = Array.isArray(result.rules) ? result.rules : [];
106
+ } catch (error) {
107
+ if (error?.status !== 404 && error?.code !== "NoSuchCORSConfiguration") {
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ if (rules.some((rule) => corsRuleAllows(rule, origins, methods))) {
113
+ console.log(` CORS: 已允许 ${origins.join(", ")}`);
114
+ return;
115
+ }
116
+
117
+ await client.putBucketCORS(ossConfig.bucket, [
118
+ ...rules,
119
+ {
120
+ allowedOrigin: origins,
121
+ allowedMethod: methods,
122
+ allowedHeader: "*",
123
+ exposeHeader: ["ETag", "Content-Length", "Content-Type"],
124
+ maxAgeSeconds: "3600",
125
+ },
126
+ ]);
127
+ console.log(` CORS: 已配置 ${origins.join(", ")}`);
128
+ }
@@ -0,0 +1,80 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { rootDir as workspaceRoot } from "./load-config.mjs";
5
+
6
+ export const rootDir = workspaceRoot;
7
+ export const srcRoot = path.join(rootDir, "src");
8
+ export const formsRoot = path.join(srcRoot, "forms");
9
+ export const pagesRoot = path.join(srcRoot, "pages");
10
+ export const distRoot = path.join(rootDir, "dist");
11
+
12
+ async function fileExists(targetPath) {
13
+ try {
14
+ await fs.access(targetPath);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ async function loadTypeScriptModule(filePath) {
22
+ const loaded = await import(pathToFileURL(filePath).href);
23
+ return loaded.default || loaded;
24
+ }
25
+
26
+ export async function discoverPages(filterName = "") {
27
+ if (!(await fileExists(pagesRoot))) {
28
+ return [];
29
+ }
30
+
31
+ const entries = await fs.readdir(pagesRoot, { withFileTypes: true });
32
+ const pages = [];
33
+
34
+ for (const entry of entries) {
35
+ if (!entry.isDirectory()) continue;
36
+ if (filterName && entry.name !== filterName) continue;
37
+
38
+ const dirPath = path.join(pagesRoot, entry.name);
39
+ const entryPath = path.join(dirPath, "index.tsx");
40
+ const appPath = path.join(dirPath, "App.tsx");
41
+ const configPath = path.join(dirPath, "page.config.ts");
42
+ const required = [entryPath, appPath, configPath];
43
+ const exists = await Promise.all(required.map(fileExists));
44
+ if (exists.every((item) => !item)) continue;
45
+ for (let index = 0; index < required.length; index += 1) {
46
+ if (!exists[index]) {
47
+ throw new Error(`页面 ${entry.name} 缺少文件: ${path.basename(required[index])}`);
48
+ }
49
+ }
50
+
51
+ const config = await loadTypeScriptModule(configPath);
52
+ if (config.publish === false) continue;
53
+ pages.push({
54
+ dirName: entry.name,
55
+ dirPath,
56
+ entryPath,
57
+ appPath,
58
+ configPath,
59
+ config,
60
+ });
61
+ }
62
+
63
+ return pages.sort((left, right) =>
64
+ String(left.config.name || left.dirName).localeCompare(
65
+ String(right.config.name || right.dirName),
66
+ "zh-Hans-CN",
67
+ ),
68
+ );
69
+ }
70
+
71
+ export async function readPackageJson() {
72
+ return JSON.parse(await fs.readFile(path.join(rootDir, "package.json"), "utf8"));
73
+ }
74
+
75
+ export async function getReactVersion() {
76
+ const packageJson = await readPackageJson();
77
+ return String(packageJson.dependencies?.react || "")
78
+ .replace(/^[~^]/, "")
79
+ .trim();
80
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * progress.mjs - 上传进度报告工具
3
+ *
4
+ * 提供文件上传进度跟踪,包括已完成数量、文件总大小、耗时统计。
5
+ */
6
+
7
+ import fs from "node:fs";
8
+
9
+ /**
10
+ * 创建进度跟踪器
11
+ * @param {number} total - 总文件数
12
+ * @returns {{ tick: (file: string, localPath: string) => void, summary: () => void }}
13
+ */
14
+ export function createProgressTracker(total) {
15
+ let completed = 0;
16
+ let totalBytes = 0;
17
+ const startTime = Date.now();
18
+
19
+ return {
20
+ /**
21
+ * 标记一个文件上传完成
22
+ * @param {string} ossKey - OSS 对象键(用于日志)
23
+ * @param {string} localPath - 本地文件路径(用于计算大小)
24
+ */
25
+ tick(ossKey, localPath) {
26
+ completed += 1;
27
+ try {
28
+ const stat = fs.statSync(localPath);
29
+ totalBytes += stat.size;
30
+ } catch {
31
+ // 忽略 stat 失败
32
+ }
33
+ const pct = Math.round((completed / total) * 100);
34
+ console.log(` ✓ [${completed}/${total} ${pct}%] ${ossKey}`);
35
+ },
36
+
37
+ /**
38
+ * 标记一个文件上传失败
39
+ * @param {string} ossKey - OSS 对象键
40
+ * @param {Error} error - 错误对象
41
+ */
42
+ fail(ossKey, error) {
43
+ completed += 1;
44
+ const pct = Math.round((completed / total) * 100);
45
+ console.error(` ✗ [${completed}/${total} ${pct}%] ${ossKey}: ${error.message}`);
46
+ },
47
+
48
+ /**
49
+ * 打印上传汇总信息
50
+ */
51
+ summary() {
52
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
53
+ const sizeMB = (totalBytes / (1024 * 1024)).toFixed(2);
54
+ console.log(`\n📊 上传统计: ${total} 个文件, ${sizeMB} MB, 耗时 ${elapsed}s`);
55
+ },
56
+ };
57
+ }
@@ -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: "shadow",
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,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
+ }