lan-low-code-cli 0.0.0-stage → 2.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.
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WidgetRegistry = void 0;
4
+ exports.originOf = originOf;
5
+ exports.collectWidgetDefaults = collectWidgetDefaults;
6
+ exports.hasConfigItem = hasConfigItem;
7
+ exports.validateRequiredProps = validateRequiredProps;
8
+ exports.elementLangKey = elementLangKey;
9
+ const fs_1 = require("fs");
10
+ const path_1 = require("path");
11
+ const config_1 = require("./config");
12
+ const constants_1 = require("./constants");
13
+ const client_1 = require("./client");
14
+ const random_1 = require("./utils/random");
15
+ const CACHE_FILE = (0, path_1.join)(config_1.GLOBAL_CONFIG_DIR, 'widgets-cache.json');
16
+ /** 由 baseUrl 推导站点根,例如 http://host/data -> http://host */
17
+ function originOf(baseUrl) {
18
+ try {
19
+ const url = new URL(baseUrl);
20
+ return `${url.protocol}//${url.host}`;
21
+ }
22
+ catch {
23
+ return baseUrl.replace(/\/data\/?$/, '').replace(/\/+$/, '');
24
+ }
25
+ }
26
+ /** 遍历控件 config.json 中的 `config` 配置树 */
27
+ function walkConfig(items, visitor) {
28
+ if (!Array.isArray(items))
29
+ return;
30
+ items.forEach((raw) => {
31
+ if (!raw || typeof raw !== 'object')
32
+ return;
33
+ const item = raw;
34
+ visitor(item);
35
+ if (Array.isArray(item.children))
36
+ walkConfig(item.children, visitor);
37
+ });
38
+ }
39
+ /**
40
+ * 提取控件 config.json 中声明的默认值。
41
+ *
42
+ * 对应前端 `mergeAnnotationDefaultToDefaultProps`:把配置项上的 `default`
43
+ * 合并成控件默认属性(如 aiFill/showText/passValue/encrypt/maskFormatType 等)。
44
+ */
45
+ function collectWidgetDefaults(meta) {
46
+ const result = {};
47
+ walkConfig(meta?.config, (item) => {
48
+ if (item.key && item.default !== undefined) {
49
+ result[item.key] = item.default;
50
+ }
51
+ });
52
+ return result;
53
+ }
54
+ /** 控件配置面板是否声明了某个配置项 */
55
+ function hasConfigItem(meta, key) {
56
+ let found = false;
57
+ walkConfig(meta?.config, (item) => {
58
+ if (item.key === key)
59
+ found = true;
60
+ });
61
+ return found;
62
+ }
63
+ /**
64
+ * 校验控件必填属性值(对齐设计器发布前校验)。
65
+ *
66
+ * 设计器的校验由属性面板表单规则驱动(`formBuilderHelper#getFormConfigDetailByConfigJson`):
67
+ * `config.json` 中满足 `!protected && !slotScope && required && !disabled` 的配置项
68
+ * 会带上 `{ required: true, message: '请输入<label>' }` 规则,错误汇总到 `validates`;
69
+ * 发布时 `dispatch.validates.validating()` 存在任何控件错误即**阻断发布**。
70
+ *
71
+ * 与设计器行为对齐的两个关键点:
72
+ * 1. antd 表单只校验**实际渲染**的属性项 —— 带 `enableWhen`(条件渲染)或 `hidden`
73
+ * 的配置项未渲染时不参与校验,CLI 同样跳过(否则会误报,如 input 的 prefix/suffix
74
+ * 仅在启用前后缀开关时才出现在属性面板);
75
+ * 2. 含 `children` 的是属性面板的分组容器(baseGroup/styleGroup 等),不承载值,跳过。
76
+ *
77
+ * 值查找顺序:`controlProps[key]` → `labelProps[key]`;
78
+ * `inputLocale` 类配置项(多语言标题)的值实际存放在 `labelProps.title`/`controlProps.title`。
79
+ */
80
+ function validateRequiredProps(meta, config) {
81
+ const controlProps = config.controlProps ?? {};
82
+ const labelProps = config.labelProps ?? {};
83
+ const errors = [];
84
+ walkConfig(meta?.config, (item) => {
85
+ if (item.protected === true || item.slotScope === true)
86
+ return;
87
+ if (!item.required || item.disabled === true)
88
+ return;
89
+ if (typeof item.key !== 'string')
90
+ return;
91
+ // 条件必填:enableWhen 为「键 = 简单值」且条件在 controlProps 中成立时才校验
92
+ // (对齐设计器——属性项按 enableWhen 渲染,渲染了才参与校验;
93
+ // 典型场景:datetime 的 defaultValueType='formula' 时公式配置 defaultValueFormulaVO 必填)
94
+ const enableWhen = item.enableWhen;
95
+ if (enableWhen !== undefined) {
96
+ if (enableWhen === null || typeof enableWhen !== 'object' || Array.isArray(enableWhen))
97
+ return;
98
+ const entries = Object.entries(enableWhen);
99
+ if (entries.length === 0)
100
+ return;
101
+ const matched = entries.every(([k, v]) => {
102
+ if (v !== null && typeof v === 'object')
103
+ return false; // 复杂条件无法静态判定,不校验
104
+ return controlProps[k] === v;
105
+ });
106
+ if (!matched)
107
+ return;
108
+ }
109
+ else if (item.hidden) {
110
+ // 隐藏的配置项:设计器中未渲染则不校验
111
+ return;
112
+ }
113
+ // 含 children 的是属性面板的分组容器(如 baseGroup/styleGroup),不承载值,跳过
114
+ if (Array.isArray(item.children) && item.children.length)
115
+ return;
116
+ let value = controlProps[item.key];
117
+ if (value === undefined || value === null || value === '') {
118
+ value = labelProps[item.key];
119
+ }
120
+ if ((value === undefined || value === null || value === '') && item.type === 'inputLocale') {
121
+ value = labelProps.title ?? controlProps.title;
122
+ }
123
+ const empty = value === undefined ||
124
+ value === null ||
125
+ value === '' ||
126
+ (Array.isArray(value) && value.length === 0);
127
+ if (empty) {
128
+ errors.push({ key: item.key, label: String(item.label ?? item.key) });
129
+ }
130
+ });
131
+ return errors;
132
+ }
133
+ /** 控件多语言资源 key 约定:`{pkg}###langs###{prop}` */
134
+ function elementLangKey(pkg, prop) {
135
+ return `${pkg}###langs###${prop}`;
136
+ }
137
+ /**
138
+ * 控件元数据注册表:按需拉取控件包 `config.json`,并落盘缓存。
139
+ */
140
+ class WidgetRegistry {
141
+ constructor(options) {
142
+ this.memory = new Map();
143
+ this.langCache = new Map();
144
+ /** 按原始语言码分组的控件多语言缓存 */
145
+ this.langsCache = new Map();
146
+ /** 环境控件包清单(来自 index.json,内存缓存) */
147
+ this.packages = null;
148
+ const opts = typeof options === 'string' ? { baseUrl: options } : options;
149
+ this.origin = originOf(opts.baseUrl);
150
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
151
+ this.token = opts.token;
152
+ this.timeout = opts.timeout ?? 30000;
153
+ this.disk = (0, random_1.safeJsonParse)(readFileSyncSafe(CACHE_FILE), {});
154
+ }
155
+ /**
156
+ * 拉取控件多语言资源,返回**按原始语言码分组**的完整映射。
157
+ *
158
+ * 对应前端 `sys-xform/src/components/ide/util/elements.ts#getDefaultPropsLocale`:
159
+ * `langHttp.post('/getFrontResource', [{ path: 'sys/artifact/elements/i18n', key: [key] }])`,
160
+ * 响应形如 `{ data: [{ data: { 'zh-cn': { [key]: '请输入' } } }] }`
161
+ * —— `data[0].data` 的键即环境提供的语言码(可用于探测可用语言)。
162
+ *
163
+ * ```ts
164
+ * await registry.getElementLangs('@elem/xform-input###langs###placeholder')
165
+ * // { 'zh-cn': { '@elem/xform-input###langs###placeholder': '请输入' } }
166
+ * ```
167
+ */
168
+ async getElementLangs(key) {
169
+ if (!this.baseUrl)
170
+ return null;
171
+ if (this.langsCache.has(key))
172
+ return this.langsCache.get(key);
173
+ let value = null;
174
+ try {
175
+ const res = await (0, client_1.rawRequest)({
176
+ url: `${this.baseUrl}/sys-lang/getFrontResource`,
177
+ method: 'POST',
178
+ headers: {
179
+ 'Content-Type': 'application/json;charset=UTF-8',
180
+ Accept: 'application/json, text/plain, */*',
181
+ ...(this.token ? { 'X-AUTH-TOKEN': this.token } : {}),
182
+ },
183
+ body: JSON.stringify([{ path: 'sys/artifact/elements/i18n', key: [key] }]),
184
+ timeout: this.timeout,
185
+ });
186
+ if (res.status >= 200 && res.status < 300) {
187
+ const body = JSON.parse(res.text);
188
+ // 响应形如 { data: [ { data: { 'zh-cn': { <key>: '请输入' }, 'en-us': {...} } } ] }
189
+ // 每个请求 key 对应一项,这里全部合并。
190
+ const result = {};
191
+ for (const item of body?.data ?? []) {
192
+ const byLocale = item?.data;
193
+ if (!byLocale || typeof byLocale !== 'object')
194
+ continue;
195
+ for (const [locale, dict] of Object.entries(byLocale)) {
196
+ const text = dict?.[key];
197
+ if (typeof text === 'string' && text)
198
+ result[locale] = text;
199
+ }
200
+ }
201
+ value = Object.keys(result).length ? result : null;
202
+ }
203
+ }
204
+ catch {
205
+ value = null;
206
+ }
207
+ this.langsCache.set(key, value);
208
+ return value;
209
+ }
210
+ static pathOf(pkg) {
211
+ const short = pkg.replace(/^@elem\//, '');
212
+ return `${constants_1.ELEMENT_ARTIFACT_PREFIX}/${short}/config.json`;
213
+ }
214
+ urlOf(pkg) {
215
+ return `${this.origin}/${WidgetRegistry.pathOf(pkg)}`;
216
+ }
217
+ /**
218
+ * 枚举环境中的控件包(`@elem/xform-*`)。
219
+ *
220
+ * 数据源是静态产物 `{origin}/web/sys/artifact/elements/index.json`,
221
+ * 条目形如 `{ name, platform, category, version }`(不含 xform 元数据,
222
+ * 需要时用 `get(pkg)` 拉取各包的 `config.json`)。
223
+ */
224
+ async listPackages() {
225
+ if (this.packages)
226
+ return this.packages;
227
+ let list = [];
228
+ try {
229
+ const res = await (0, client_1.rawRequest)({
230
+ url: `${this.origin}/${constants_1.ELEMENT_ARTIFACT_PREFIX}/index.json`,
231
+ method: 'GET',
232
+ headers: { Accept: 'application/json' },
233
+ timeout: this.timeout,
234
+ });
235
+ if (res.status >= 200 && res.status < 300) {
236
+ const entries = JSON.parse(res.text);
237
+ list = (entries || [])
238
+ .filter((item) => item.name && item.name.startsWith('@elem/xform-') && !item.name.includes('~'))
239
+ .map((item) => ({
240
+ name: item.name,
241
+ platform: item.platform ?? '',
242
+ category: item.category ?? '',
243
+ version: item.version ?? '',
244
+ }));
245
+ }
246
+ }
247
+ catch {
248
+ list = [];
249
+ }
250
+ this.packages = list;
251
+ return list;
252
+ }
253
+ /** 获取控件元数据;失败返回 null(由调用方走兜底逻辑) */
254
+ async get(pkg) {
255
+ if (!pkg)
256
+ return null;
257
+ if (this.memory.has(pkg))
258
+ return this.memory.get(pkg);
259
+ const cacheKey = `${this.origin}|${pkg}`;
260
+ if (Object.prototype.hasOwnProperty.call(this.disk, cacheKey)) {
261
+ const cached = this.disk[cacheKey];
262
+ this.memory.set(pkg, cached);
263
+ return cached;
264
+ }
265
+ let result = null;
266
+ try {
267
+ const res = await (0, client_1.rawRequest)({
268
+ url: this.urlOf(pkg),
269
+ method: 'GET',
270
+ headers: { Accept: 'application/json' },
271
+ timeout: this.timeout,
272
+ });
273
+ if (res.status >= 200 && res.status < 300) {
274
+ result = JSON.parse(res.text);
275
+ }
276
+ }
277
+ catch {
278
+ result = null;
279
+ }
280
+ this.memory.set(pkg, result);
281
+ this.disk[cacheKey] = result;
282
+ this.flush();
283
+ return result;
284
+ }
285
+ flush() {
286
+ try {
287
+ if (!(0, fs_1.existsSync)(config_1.GLOBAL_CONFIG_DIR))
288
+ (0, fs_1.mkdirSync)(config_1.GLOBAL_CONFIG_DIR, { recursive: true });
289
+ (0, fs_1.writeFileSync)(CACHE_FILE, JSON.stringify(this.disk, null, 2), 'utf-8');
290
+ }
291
+ catch {
292
+ /* 缓存写入失败不影响主流程 */
293
+ }
294
+ }
295
+ }
296
+ exports.WidgetRegistry = WidgetRegistry;
297
+ function readFileSyncSafe(file) {
298
+ try {
299
+ return (0, fs_1.existsSync)(file) ? (0, fs_1.readFileSync)(file, 'utf-8') : null;
300
+ }
301
+ catch {
302
+ return null;
303
+ }
304
+ }
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "lan-low-code-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "2.0.0",
4
+ "description": "lan-low-code cli:低代码表单引擎命令行工具(创建表单、获取可用字段、自动完善表单字段)",
5
+ "keywords": [
6
+ "low-code",
7
+ "lan-low-code",
8
+ "llc",
9
+ "form",
10
+ "cli"
11
+ ],
12
+ "license": "MIT",
13
+ "bin": {
14
+ "llc": "bin/llc.js"
15
+ },
16
+ "main": "dist/index.js",
17
+ "dependencies": {
18
+ "commander": "^11.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.11.30",
22
+ "rimraf": "^5.0.5",
23
+ "typescript": "^5.4.5"
24
+ },
25
+ "engines": {
26
+ "node": ">=18",
27
+ "pnpm": ">=8"
28
+ },
29
+ "files": [
30
+ "bin",
31
+ "dist",
32
+ "README.md"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.json",
36
+ "clean": "rimraf dist",
37
+ "rebuild": "pnpm run clean && pnpm run build",
38
+ "start": "pnpm run build && node bin/llc.js",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit",
40
+ "bump": "node scripts/bump-version.js",
41
+ "bump:check": "node scripts/bump-version.js --check"
42
+ }
6
43
  }