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,59 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { rootDir } from "./utils/load-config.mjs";
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
9
+ const tsxCli = require.resolve("tsx/cli");
10
+ const args = process.argv.slice(2).filter((arg) => arg !== "--");
11
+ const forwardedArgs = [];
12
+
13
+ if (args.includes("--help") || args.includes("-h")) {
14
+ console.log(`
15
+ build - 构建表单页和代码页
16
+
17
+ 用法:
18
+ lowcode-workspace build [options]
19
+
20
+ 选项:
21
+ --bundle-mode shared 或 self-contained,默认 shared
22
+ --no-runtime-cache 强制重建共享 runtime
23
+ --help, -h 显示帮助信息
24
+ `);
25
+ process.exit(0);
26
+ }
27
+
28
+ for (let index = 0; index < args.length; index += 1) {
29
+ const arg = args[index];
30
+ if (arg === "--bundle-mode" && args[index + 1]) {
31
+ forwardedArgs.push(arg, args[index + 1]);
32
+ index += 1;
33
+ continue;
34
+ }
35
+ if (arg === "--no-runtime-cache") {
36
+ forwardedArgs.push(arg);
37
+ }
38
+ }
39
+
40
+ const runScript = (scriptName, args) =>
41
+ new Promise((resolve, reject) => {
42
+ const child = spawn(process.execPath, [tsxCli, path.join(scriptDir, scriptName), ...args], {
43
+ cwd: rootDir,
44
+ env: process.env,
45
+ shell: false,
46
+ stdio: "inherit",
47
+ });
48
+ child.on("error", reject);
49
+ child.on("close", (code) => {
50
+ if (code === 0) {
51
+ resolve();
52
+ return;
53
+ }
54
+ reject(new Error(`${scriptName} ${args.join(" ")} exited with code ${code}`));
55
+ });
56
+ });
57
+
58
+ await runScript("build-forms.mjs", forwardedArgs);
59
+ await runScript("build-pages.mjs", forwardedArgs);
@@ -0,0 +1,111 @@
1
+ /**
2
+ * publish-all.mjs - 统一执行 schema 同步、构建、OSS 发布和平台注册。
3
+ *
4
+ * 关键点:在同一个进程里生成一次 APP_BUILD_ID,并传给所有子脚本,
5
+ * 避免上传路径和注册 URL 因跨进程默认 buildId 不一致而错位。
6
+ */
7
+
8
+ import { spawn } from "node:child_process";
9
+ import { createRequire } from "node:module";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import minimist from "minimist";
13
+ import { rootDir } from "./utils/load-config.mjs";
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
17
+ const tsxCli = require.resolve("tsx/cli");
18
+
19
+ /**
20
+ * 生成时间戳格式的构建ID
21
+ * @returns {string} 格式如 20260516120000
22
+ */
23
+ function createBuildId() {
24
+ return new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
25
+ }
26
+
27
+ const args = minimist(process.argv.slice(2).filter((arg) => arg !== "--"));
28
+ const dryRun = Boolean(args["dry-run"]);
29
+ const targetForm = args.form || "";
30
+ const targetPage = args.page || "";
31
+ const bundleModeArgs = args["bundle-mode"]
32
+ ? ["--bundle-mode", String(args["bundle-mode"])]
33
+ : [];
34
+
35
+ if (args.help || args.h) {
36
+ console.log(`
37
+ publish-all - 同步、构建、上传并注册应用工作区产物
38
+
39
+ 用法:
40
+ tsx scripts/publish-all.mjs [options]
41
+
42
+ 选项:
43
+ --dry-run schema、OSS、注册均只打印计划,不调用写接口
44
+ --form <name> 只发布指定表单
45
+ --page <name> 只发布指定代码页
46
+ --bundle-mode shared 或 self-contained,默认 shared
47
+ --help, -h 显示帮助信息
48
+ `);
49
+ process.exit(0);
50
+ }
51
+
52
+ if (targetForm && targetPage) {
53
+ console.error("❌ --form 和 --page 不能同时使用,请分两次发布");
54
+ process.exit(1);
55
+ }
56
+
57
+ const buildId = process.env.APP_BUILD_ID || createBuildId();
58
+ const childEnv = {
59
+ ...process.env,
60
+ APP_BUILD_ID: buildId,
61
+ };
62
+
63
+ /**
64
+ * 执行子脚本
65
+ * @param {string} script - 脚本路径
66
+ * @param {string[]} [scriptArgs] - 脚本参数
67
+ * @returns {Promise<void>}
68
+ */
69
+ function run(script, scriptArgs = []) {
70
+ return new Promise((resolve, reject) => {
71
+ const child = spawn(process.execPath, [tsxCli, path.join(scriptDir, script), ...scriptArgs], {
72
+ cwd: rootDir,
73
+ env: childEnv,
74
+ shell: false,
75
+ stdio: "inherit",
76
+ });
77
+ child.on("error", reject);
78
+ child.on("close", (code) => {
79
+ if (code === 0) {
80
+ resolve();
81
+ return;
82
+ }
83
+ reject(new Error(`${script} ${scriptArgs.join(" ")} exited with code ${code}`));
84
+ });
85
+ });
86
+ }
87
+
88
+ const maybeDryRun = dryRun ? ["--dry-run"] : [];
89
+
90
+ console.log(`🚀 发布应用工作区 (${dryRun ? "DRY RUN" : "LIVE"})`);
91
+ console.log(` APP_BUILD_ID=${buildId}`);
92
+ console.log("");
93
+
94
+ if (targetPage) {
95
+ await run("build-pages.mjs", ["--page", targetPage, ...bundleModeArgs]);
96
+ await run("publish-oss.mjs", ["--page", targetPage, ...maybeDryRun]);
97
+ await run("register.mjs", ["--page", targetPage, ...maybeDryRun]);
98
+ } else if (targetForm) {
99
+ await run("sync-schema.mjs", ["--form", targetForm, ...maybeDryRun]);
100
+ await run("build-forms.mjs", ["--form", targetForm, ...bundleModeArgs]);
101
+ await run("publish-oss.mjs", ["--form", targetForm, ...maybeDryRun]);
102
+ await run("register.mjs", ["--form", targetForm, ...maybeDryRun]);
103
+ } else {
104
+ await run("sync-schema.mjs", maybeDryRun);
105
+ await run("build-workspace.mjs", bundleModeArgs);
106
+ await run("publish-oss.mjs", maybeDryRun);
107
+ await run("register.mjs", maybeDryRun);
108
+ }
109
+
110
+ console.log("");
111
+ console.log(`✅ 发布流程完成,buildId=${buildId}`);
@@ -0,0 +1,143 @@
1
+ /**
2
+ * publish-oss.mjs - 上传应用工作区构建产物到阿里云 OSS
3
+ *
4
+ * 功能特性:
5
+ * - 完整 MIME 类型识别(JS/CSS/图片/字体等)
6
+ * - 基于文件名 hash 的差异化 Cache-Control 策略
7
+ * - 并行上传(p-limit 控制并发数)
8
+ * - 失败自动重试
9
+ * - 上传进度与统计报告
10
+ */
11
+
12
+ import { glob } from "glob";
13
+ import path from "node:path";
14
+ import minimist from "minimist";
15
+ import pLimit from "p-limit";
16
+ import { loadConfig, rootDir } from "./utils/load-config.mjs";
17
+ import { getContentType, getCacheControl } from "./utils/mime-types.mjs";
18
+ import { createOSSClient, uploadWithRetry, ensureBucketCors } from "./utils/oss-client.mjs";
19
+ import { createProgressTracker } from "./utils/progress.mjs";
20
+
21
+ const args = minimist(process.argv.slice(2).filter((arg) => arg !== "--"));
22
+ const dryRun = Boolean(args["dry-run"]);
23
+ const targetForm = args.form || null;
24
+ const targetPage = args.page || null;
25
+
26
+ if (args.help || args.h) {
27
+ console.log(`
28
+ publish-oss - 发布应用工作区产物到阿里云 OSS
29
+
30
+ 用法:
31
+ tsx scripts/publish-oss.mjs [options]
32
+
33
+ 选项:
34
+ --dry-run 只打印上传计划,不实际上传
35
+ --form <name> 只上传指定表单
36
+ --page <name> 只上传指定代码页
37
+ --help, -h 显示帮助信息
38
+ `);
39
+ process.exit(0);
40
+ }
41
+
42
+ const config = await loadConfig();
43
+
44
+ if (!dryRun && !config.buildIdExplicit) {
45
+ console.error(
46
+ "❌ 单独执行 publish:oss 时必须设置 APP_BUILD_ID;推荐使用 pnpm publish:all 统一发布。",
47
+ );
48
+ process.exit(1);
49
+ }
50
+
51
+ if (
52
+ !dryRun &&
53
+ (!config.oss.accessKeyId || config.oss.accessKeyId.startsWith("your_"))
54
+ ) {
55
+ console.error("❌ 请先配置 OSS 密钥(.env 文件中的 APP_OSS_* 字段)");
56
+ process.exit(1);
57
+ }
58
+
59
+ const client = dryRun ? null : createOSSClient(config.oss);
60
+
61
+ /**
62
+ * 构建文件扫描 glob 模式
63
+ * @returns {string} glob 模式
64
+ */
65
+ function buildPattern() {
66
+ if (targetForm) return `{form-runtime/**/*,forms/${targetForm}/**/*}`;
67
+ if (targetPage) return `{page-runtime/**/*,pages/${targetPage}/**/*}`;
68
+ return "{form-runtime,page-runtime,forms,pages}/**/*";
69
+ }
70
+
71
+ const distDir = path.resolve(rootDir, "dist");
72
+ const files = (await glob(buildPattern(), { cwd: distDir, nodir: true })).filter(
73
+ (file) => !file.endsWith("/build-cache.json"),
74
+ );
75
+
76
+ if (files.length === 0) {
77
+ console.error("❌ 没有找到构建产物。请先运行 pnpm build");
78
+ process.exit(1);
79
+ }
80
+
81
+ const remoteRoot = `${config.oss.pathPrefix}/${config.version}/${config.buildId}`;
82
+
83
+ console.log(
84
+ `📦 准备上传 ${files.length} 个文件到 OSS (${dryRun ? "DRY RUN" : "LIVE"})`,
85
+ );
86
+ console.log(` Bucket: ${config.oss.bucket}`);
87
+ console.log(` Path: ${remoteRoot}/`);
88
+ console.log("");
89
+
90
+ // ---------- Dry-run 模式 ----------
91
+
92
+ if (dryRun) {
93
+ for (const file of files) {
94
+ const ossKey = `${remoteRoot}/${file}`;
95
+ const mime = getContentType(file);
96
+ const cache = getCacheControl(file);
97
+ console.log(` [DRY] ${ossKey} (${mime}, ${cache})`);
98
+ }
99
+ console.log(`\n✅ 预览完成(${files.length} 个文件)`);
100
+ process.exit(0);
101
+ }
102
+
103
+ // ---------- 实际上传 ----------
104
+
105
+ await ensureBucketCors(client, config.oss);
106
+ console.log("");
107
+
108
+ const limit = pLimit(5); // 最多 5 个并发上传
109
+ const tracker = createProgressTracker(files.length);
110
+
111
+ const results = await Promise.allSettled(
112
+ files.map((file) =>
113
+ limit(async () => {
114
+ const ossKey = `${remoteRoot}/${file}`;
115
+ const localPath = path.join(distDir, file);
116
+ const headers = {
117
+ "Cache-Control": getCacheControl(file),
118
+ "Content-Type": getContentType(file),
119
+ };
120
+
121
+ await uploadWithRetry(client, ossKey, localPath, headers);
122
+ tracker.tick(ossKey, localPath);
123
+ }),
124
+ ),
125
+ );
126
+
127
+ // 报告失败项
128
+ const failures = results.filter((r) => r.status === "rejected");
129
+ if (failures.length > 0) {
130
+ console.error(`\n⚠ ${failures.length} 个文件上传失败:`);
131
+ for (const f of failures) {
132
+ console.error(` - ${f.reason?.message || f.reason}`);
133
+ }
134
+ }
135
+
136
+ tracker.summary();
137
+
138
+ const successCount = results.filter((r) => r.status === "fulfilled").length;
139
+ console.log(`\n✅ 上传完成(${successCount}/${files.length} 成功)`);
140
+
141
+ if (failures.length > 0) {
142
+ process.exit(1);
143
+ }
@@ -0,0 +1 @@
1
+ import "./register.mjs";
@@ -0,0 +1,242 @@
1
+ /**
2
+ * register.mjs - 注册表单 bundle 和复杂代码页到平台
3
+ */
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { glob } from "glob";
8
+ import minimist from "minimist";
9
+ import {
10
+ getApiBaseUrl,
11
+ getFormBundleUrl,
12
+ getFormRuntimeUrl,
13
+ loadConfig,
14
+ rootDir,
15
+ } from "./utils/load-config.mjs";
16
+ import { discoverPages } from "./utils/pages.mjs";
17
+ import { buildDirectPagePublishPayload } from "./utils/register-payload.mjs";
18
+ import {
19
+ ensureSchemaFormUuid,
20
+ getOpenApiAccessToken,
21
+ getOpenApiForm,
22
+ } from "./utils/form-api.mjs";
23
+ import { assertFormReadyForBundle } from "./utils/schema-transform.mjs";
24
+
25
+ const args = minimist(process.argv.slice(2).filter((arg) => arg !== "--"));
26
+ const dryRun = Boolean(args["dry-run"]);
27
+ const targetForm = args.form || null;
28
+ const targetPage = args.page || null;
29
+
30
+ if (args.help || args.h) {
31
+ console.log(`
32
+ register - 注册应用工作区产物到平台
33
+
34
+ 用法:
35
+ tsx scripts/register.mjs [options]
36
+
37
+ 选项:
38
+ --dry-run 只打印注册计划,不实际调用 API
39
+ --form <name> 只注册指定表单
40
+ --page <name> 只注册指定代码页目录
41
+ --help, -h 显示帮助信息
42
+ `);
43
+ process.exit(0);
44
+ }
45
+
46
+ const config = await loadConfig();
47
+ const apiBase = getApiBaseUrl(config);
48
+ let accessToken = null;
49
+
50
+ if (!dryRun && !config.buildIdExplicit) {
51
+ console.error(
52
+ "❌ 单独执行 register 时必须设置 APP_BUILD_ID;推荐使用 pnpm publish:all 统一发布。",
53
+ );
54
+ process.exit(1);
55
+ }
56
+
57
+ async function getAccessToken() {
58
+ if (!accessToken) {
59
+ accessToken = await getOpenApiAccessToken(config);
60
+ }
61
+ return accessToken;
62
+ }
63
+
64
+ async function registerForms() {
65
+ const formDirs = await glob(targetForm ? targetForm : "*", {
66
+ cwd: path.resolve(rootDir, "src/forms"),
67
+ onlyDirectories: true,
68
+ });
69
+
70
+ let succeeded = 0;
71
+ let failed = 0;
72
+ let created = 0;
73
+
74
+ for (const formName of formDirs) {
75
+ const schemaPath = path.resolve(rootDir, `src/forms/${formName}/schema.ts`);
76
+ if (!fs.existsSync(schemaPath)) {
77
+ console.log(` ⊘ 表单 ${formName}: 无 schema.ts,跳过注册`);
78
+ continue;
79
+ }
80
+
81
+ let formUuid;
82
+ let appType;
83
+ try {
84
+ const ensured = await ensureSchemaFormUuid({
85
+ config,
86
+ schemaPath,
87
+ formName,
88
+ accessToken: dryRun ? null : await getAccessToken(),
89
+ dryRun,
90
+ });
91
+ formUuid = ensured.formUuid;
92
+ appType = ensured.appType || config.appType;
93
+ if (ensured.created || ensured.dryRunCreated) created += 1;
94
+ } catch (error) {
95
+ console.error(` ✗ 表单 ${formName}: ${error.message}`);
96
+ failed += 1;
97
+ continue;
98
+ }
99
+
100
+ if (!dryRun) {
101
+ try {
102
+ const formMeta = await getOpenApiForm(config, await getAccessToken(), {
103
+ appType,
104
+ formUuid,
105
+ });
106
+ assertFormReadyForBundle(formMeta, formName);
107
+ } catch (error) {
108
+ console.error(` ✗ 表单 ${formName}: ${error.message}`);
109
+ failed += 1;
110
+ continue;
111
+ }
112
+ }
113
+
114
+ const payload = {
115
+ appType,
116
+ formUuid,
117
+ userId: config.userId,
118
+ bundleUrl: getFormBundleUrl(config, formName, "index.js"),
119
+ cssUrl: getFormBundleUrl(config, formName, "style.css"),
120
+ version: config.version,
121
+ };
122
+ const runtime = readFormRuntimeAssets(config);
123
+ if (runtime) {
124
+ payload.runtimeMode = "shared";
125
+ payload.runtime = runtime;
126
+ }
127
+
128
+ if (dryRun) {
129
+ console.log(` [DRY] 表单 ${formName} (${formUuid})`);
130
+ console.log(` → ${payload.bundleUrl}`);
131
+ console.log(` → ${payload.cssUrl}`);
132
+ if (payload.runtime) {
133
+ console.log(` runtime → ${payload.runtime.entryUrl}`);
134
+ if (payload.runtime.cssUrl) {
135
+ console.log(` runtime → ${payload.runtime.cssUrl}`);
136
+ }
137
+ }
138
+ succeeded += 1;
139
+ continue;
140
+ }
141
+
142
+ const response = await fetch(`${apiBase}/dingtalk-api/v1.0/forms/customPage/publish`, {
143
+ method: "POST",
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ "x-acs-dingtalk-access-token": await getAccessToken(),
147
+ },
148
+ body: JSON.stringify(payload),
149
+ });
150
+ const body = await response.json().catch(() => null);
151
+ if (!response.ok || body?.code !== 200) {
152
+ console.error(
153
+ ` ✗ 表单 ${formName}: HTTP ${response.status} ${body?.message || response.statusText}`,
154
+ );
155
+ failed += 1;
156
+ continue;
157
+ }
158
+ console.log(` ✓ 表单 ${formName} (${formUuid})`);
159
+ succeeded += 1;
160
+ }
161
+
162
+ return { succeeded, failed, created };
163
+ }
164
+
165
+ function readFormRuntimeAssets(config) {
166
+ const manifestPath = path.resolve(rootDir, "dist/form-runtime/manifest.json");
167
+ if (!fs.existsSync(manifestPath)) return null;
168
+ try {
169
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
170
+ if (
171
+ manifest?.protocol !== "sy-form-runtime" ||
172
+ manifest?.majorVersion !== 2 ||
173
+ !manifest?.version ||
174
+ !manifest?.files?.entry
175
+ ) {
176
+ return null;
177
+ }
178
+ return {
179
+ protocol: "sy-form-runtime",
180
+ majorVersion: 2,
181
+ version: manifest.version,
182
+ entryUrl: getFormRuntimeUrl(config, manifest.files.entry),
183
+ cssUrl: manifest.files.css ? getFormRuntimeUrl(config, manifest.files.css) : null,
184
+ };
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ async function registerPages() {
191
+ const pages = await discoverPages(targetPage || "");
192
+ if (pages.length === 0) {
193
+ return { succeeded: 0, failed: 0 };
194
+ }
195
+
196
+ const payload = buildDirectPagePublishPayload(config, pages);
197
+
198
+ if (dryRun) {
199
+ console.log(" [DRY] 代码页发布 payload:");
200
+ console.log(JSON.stringify(payload, null, 2));
201
+ return { succeeded: pages.length, failed: 0 };
202
+ }
203
+
204
+ const response = await fetch(`${apiBase}/dingtalk-api/v1.0/custom-pages/publish`, {
205
+ method: "POST",
206
+ headers: {
207
+ "Content-Type": "application/json",
208
+ "x-acs-dingtalk-access-token": await getAccessToken(),
209
+ },
210
+ body: JSON.stringify(payload),
211
+ });
212
+ const body = await response.json().catch(() => null);
213
+ if (!response.ok || body?.code !== 200) {
214
+ console.error(
215
+ ` ✗ 代码页发布失败: HTTP ${response.status} ${body?.message || response.statusText}`,
216
+ );
217
+ return { succeeded: 0, failed: pages.length };
218
+ }
219
+
220
+ body.data?.items?.forEach((item) => {
221
+ console.log(` ✓ 代码页 ${item.code} (${item.pageId})`);
222
+ });
223
+ return { succeeded: pages.length, failed: 0 };
224
+ }
225
+
226
+ console.log(`🔗 注册应用工作区产物 (${dryRun ? "DRY RUN" : "LIVE"})`);
227
+ console.log(` API: ${apiBase}`);
228
+ console.log("");
229
+
230
+ const formResult = targetPage ? { succeeded: 0, failed: 0, created: 0 } : await registerForms();
231
+ const pageResult = targetForm ? { succeeded: 0, failed: 0 } : await registerPages();
232
+ const succeeded = formResult.succeeded + pageResult.succeeded;
233
+ const failed = formResult.failed + pageResult.failed;
234
+
235
+ console.log("");
236
+ console.log(
237
+ `完成: ${succeeded} 成功, ${formResult.created || 0} 自动创建表单, ${failed} 失败`,
238
+ );
239
+
240
+ if (failed > 0) {
241
+ process.exit(1);
242
+ }