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.
- package/README.md +9 -0
- package/bin/lowcode-workspace.mjs +7 -0
- package/package.json +34 -0
- package/scripts/build-forms.mjs +756 -0
- package/scripts/build-pages.mjs +691 -0
- package/scripts/build-workspace.mjs +59 -0
- package/scripts/publish-all.mjs +111 -0
- package/scripts/publish-oss.mjs +143 -0
- package/scripts/register-bundle.mjs +1 -0
- package/scripts/register.mjs +242 -0
- package/scripts/sync-schema.mjs +287 -0
- package/scripts/utils/form-api.mjs +482 -0
- package/scripts/utils/load-config.mjs +210 -0
- package/scripts/utils/mime-types.mjs +70 -0
- package/scripts/utils/oss-client.mjs +128 -0
- package/scripts/utils/pages.mjs +80 -0
- package/scripts/utils/progress.mjs +57 -0
- package/scripts/utils/register-payload.mjs +89 -0
- package/scripts/utils/register-payload.test.ts +76 -0
- package/scripts/utils/schema-transform.mjs +130 -0
- package/scripts/utils/schema-transform.test.ts +141 -0
- package/src/cli.mjs +382 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build-forms.mjs - 构建单个或全部表单页面
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { gzipSync } from "node:zlib";
|
|
11
|
+
import { build } from "vite";
|
|
12
|
+
import { rootDir } from "./utils/load-config.mjs";
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const formsDir = path.join(rootDir, "src/forms");
|
|
16
|
+
const distDir = path.join(rootDir, "dist/forms");
|
|
17
|
+
const runtimeDistDir = path.join(rootDir, "dist/form-runtime");
|
|
18
|
+
const tmpDir = path.join(rootDir, ".tmp");
|
|
19
|
+
const runtimeVersionPlaceholder = "__SY_FORM_RUNTIME_VERSION__";
|
|
20
|
+
const runtimeCacheFileName = "build-cache.json";
|
|
21
|
+
|
|
22
|
+
const validBundleModes = new Set(["shared", "self-contained"]);
|
|
23
|
+
const runtimePackages = [
|
|
24
|
+
"react",
|
|
25
|
+
"react-dom",
|
|
26
|
+
"@ant-design/cssinjs",
|
|
27
|
+
"antd",
|
|
28
|
+
"antd-mobile",
|
|
29
|
+
"sy-form-components",
|
|
30
|
+
"@tiptap/react",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function formatBytes(bytes) {
|
|
34
|
+
if (!Number.isFinite(bytes)) return "-";
|
|
35
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
36
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
|
37
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatDuration(startTime) {
|
|
41
|
+
return `${((Date.now() - startTime) / 1000).toFixed(2)}s`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fileSizeLabel(filePath) {
|
|
45
|
+
if (!fs.existsSync(filePath)) return "未生成";
|
|
46
|
+
return formatBytes(fs.statSync(filePath).size);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fileGzipSizeLabel(filePath) {
|
|
50
|
+
if (!fs.existsSync(filePath)) return "未生成";
|
|
51
|
+
return formatBytes(gzipSync(fs.readFileSync(filePath)).length);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readTextIfExists(filePath) {
|
|
55
|
+
if (!fs.existsSync(filePath)) return "";
|
|
56
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readPackageVersion(packageName) {
|
|
60
|
+
try {
|
|
61
|
+
let current = path.dirname(require.resolve(packageName));
|
|
62
|
+
while (current && current !== path.dirname(current)) {
|
|
63
|
+
const packageJsonPath = path.join(current, "package.json");
|
|
64
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
65
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")).version || "unknown";
|
|
66
|
+
}
|
|
67
|
+
current = path.dirname(current);
|
|
68
|
+
}
|
|
69
|
+
return "unknown";
|
|
70
|
+
} catch {
|
|
71
|
+
return "missing";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createRuntimeInputHash(runtimeEntryContent) {
|
|
76
|
+
const hash = crypto.createHash("sha256");
|
|
77
|
+
const configFiles = [
|
|
78
|
+
path.join(rootDir, "src/index.css"),
|
|
79
|
+
path.join(rootDir, "tailwind.config.cjs"),
|
|
80
|
+
path.join(rootDir, "postcss.config.cjs"),
|
|
81
|
+
fileURLToPath(import.meta.url),
|
|
82
|
+
];
|
|
83
|
+
const packageVersions = runtimePackages.reduce((result, packageName) => {
|
|
84
|
+
result[packageName] = readPackageVersion(packageName);
|
|
85
|
+
return result;
|
|
86
|
+
}, {});
|
|
87
|
+
|
|
88
|
+
hash.update("sy-form-runtime-v2-input");
|
|
89
|
+
hash.update(runtimeEntryContent);
|
|
90
|
+
hash.update(JSON.stringify(packageVersions));
|
|
91
|
+
configFiles.forEach((filePath) => {
|
|
92
|
+
hash.update(filePath);
|
|
93
|
+
hash.update(readTextIfExists(filePath));
|
|
94
|
+
});
|
|
95
|
+
return hash.digest("hex");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function createCssOptions() {
|
|
99
|
+
const tailwindcssModule = await import("tailwindcss");
|
|
100
|
+
const autoprefixerModule = await import("autoprefixer");
|
|
101
|
+
const tailwindcss = tailwindcssModule.default ?? tailwindcssModule;
|
|
102
|
+
const autoprefixer = autoprefixerModule.default ?? autoprefixerModule;
|
|
103
|
+
return {
|
|
104
|
+
postcss: {
|
|
105
|
+
plugins: [
|
|
106
|
+
tailwindcss({ config: path.join(rootDir, "tailwind.config.cjs") }),
|
|
107
|
+
autoprefixer(),
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function createReactPlugin() {
|
|
114
|
+
const reactPluginModule = await import("@vitejs/plugin-react");
|
|
115
|
+
return reactPluginModule.default();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeBundleMode(value) {
|
|
119
|
+
const mode = String(value || "shared").trim();
|
|
120
|
+
if (validBundleModes.has(mode)) return mode;
|
|
121
|
+
throw new Error(
|
|
122
|
+
`不支持的构建模式: ${mode},可选值: ${Array.from(validBundleModes).join(", ")}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseArgs(argv) {
|
|
127
|
+
const result = {
|
|
128
|
+
form: "",
|
|
129
|
+
bundleMode: normalizeBundleMode(process.env.APP_BUNDLE_MODE),
|
|
130
|
+
runtimeCache: process.env.APP_RUNTIME_CACHE !== "false",
|
|
131
|
+
help: false,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
135
|
+
const arg = argv[i];
|
|
136
|
+
|
|
137
|
+
if (arg === "--help" || arg === "-h") {
|
|
138
|
+
result.help = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (arg === "--form" && argv[i + 1]) {
|
|
142
|
+
result.form = argv[i + 1];
|
|
143
|
+
i += 1;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (arg === "--bundle-mode" && argv[i + 1]) {
|
|
147
|
+
result.bundleMode = normalizeBundleMode(argv[i + 1]);
|
|
148
|
+
i += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (arg === "--no-runtime-cache") {
|
|
152
|
+
result.runtimeCache = false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function printHelp() {
|
|
160
|
+
console.log(`
|
|
161
|
+
build-forms - 构建表单页面
|
|
162
|
+
|
|
163
|
+
用法:
|
|
164
|
+
tsx scripts/build-forms.mjs [options]
|
|
165
|
+
|
|
166
|
+
选项:
|
|
167
|
+
--form <name> 只构建指定表单(src/forms/ 下的目录名)
|
|
168
|
+
--bundle-mode shared 或 self-contained,默认 shared
|
|
169
|
+
--no-runtime-cache 强制重建共享 runtime
|
|
170
|
+
--help, -h 显示帮助信息
|
|
171
|
+
`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function discoverForms(filterName) {
|
|
175
|
+
if (!fs.existsSync(formsDir)) {
|
|
176
|
+
console.error(`错误: 找不到表单目录 ${formsDir}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const entries = fs.readdirSync(formsDir, { withFileTypes: true });
|
|
181
|
+
const formDirs = entries
|
|
182
|
+
.filter((entry) => entry.isDirectory())
|
|
183
|
+
.filter((entry) => (filterName ? entry.name === filterName : true))
|
|
184
|
+
.filter((entry) => {
|
|
185
|
+
const pagePath = path.join(formsDir, entry.name, "page.tsx");
|
|
186
|
+
const schemaPath = path.join(formsDir, entry.name, "schema.ts");
|
|
187
|
+
return fs.existsSync(pagePath) || fs.existsSync(schemaPath);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return formDirs.map((entry) => ({
|
|
191
|
+
name: entry.name,
|
|
192
|
+
entryPath: path.join(formsDir, entry.name, "page.tsx"),
|
|
193
|
+
schemaPath: path.join(formsDir, entry.name, "schema.ts"),
|
|
194
|
+
outDir: path.join(distDir, entry.name),
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function createRuntimeEntryContent() {
|
|
199
|
+
return `import React from 'react';
|
|
200
|
+
import { createRoot } from 'react-dom/client';
|
|
201
|
+
import { StyleProvider } from '@ant-design/cssinjs';
|
|
202
|
+
import { App as AntdApp, ConfigProvider } from 'antd';
|
|
203
|
+
import zhCN from 'antd/locale/zh_CN';
|
|
204
|
+
import * as SyFormComponentsModule from 'sy-form-components';
|
|
205
|
+
import '../src/index.css';
|
|
206
|
+
|
|
207
|
+
const runtimeVersion = '${runtimeVersionPlaceholder}';
|
|
208
|
+
const roots = new WeakMap();
|
|
209
|
+
const { StandardFormPage, defineFormSchema } = SyFormComponentsModule;
|
|
210
|
+
|
|
211
|
+
function getStyleContainer(el) {
|
|
212
|
+
const rootNode = el.getRootNode?.();
|
|
213
|
+
return typeof ShadowRoot !== 'undefined' && rootNode instanceof ShadowRoot
|
|
214
|
+
? rootNode
|
|
215
|
+
: document.head;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function renderStandardForm(el, schemaInput, context = {}) {
|
|
219
|
+
let root = roots.get(el);
|
|
220
|
+
if (!root) {
|
|
221
|
+
root = createRoot(el);
|
|
222
|
+
roots.set(el, root);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const schema = defineFormSchema(schemaInput);
|
|
226
|
+
root.render(
|
|
227
|
+
<StyleProvider layer container={getStyleContainer(el)}>
|
|
228
|
+
<ConfigProvider locale={zhCN} theme={{ cssVar: {} }}>
|
|
229
|
+
<AntdApp>
|
|
230
|
+
<StandardFormPage
|
|
231
|
+
schema={schema}
|
|
232
|
+
mode={context.mode || 'submit'}
|
|
233
|
+
initialValues={context.initialValues}
|
|
234
|
+
permissions={context.permissions}
|
|
235
|
+
formUuid={context.formUuid}
|
|
236
|
+
appType={context.appType}
|
|
237
|
+
formInstanceId={context.formInstanceId}
|
|
238
|
+
onSubmit={context.onSubmit}
|
|
239
|
+
inDrawer={context.inDrawer}
|
|
240
|
+
/>
|
|
241
|
+
</AntdApp>
|
|
242
|
+
</ConfigProvider>
|
|
243
|
+
</StyleProvider>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function unmountStandardForm(el) {
|
|
248
|
+
const root = roots.get(el);
|
|
249
|
+
if (root) {
|
|
250
|
+
root.unmount();
|
|
251
|
+
roots.delete(el);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function createStandardFormModule(schemaInput) {
|
|
256
|
+
let currentEl = null;
|
|
257
|
+
return {
|
|
258
|
+
mount(el, context = {}) {
|
|
259
|
+
currentEl = el;
|
|
260
|
+
renderStandardForm(el, schemaInput, context);
|
|
261
|
+
},
|
|
262
|
+
update(el, nextContext = {}) {
|
|
263
|
+
const target = currentEl || el;
|
|
264
|
+
if (target) {
|
|
265
|
+
renderStandardForm(target, schemaInput, nextContext);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
unmount(el) {
|
|
269
|
+
const target = el || currentEl;
|
|
270
|
+
if (target) {
|
|
271
|
+
unmountStandardForm(target);
|
|
272
|
+
}
|
|
273
|
+
currentEl = null;
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export const formRuntime = {
|
|
279
|
+
protocol: 'sy-form-runtime',
|
|
280
|
+
majorVersion: 2,
|
|
281
|
+
version: runtimeVersion,
|
|
282
|
+
modules: {
|
|
283
|
+
'sy-form-components': SyFormComponentsModule,
|
|
284
|
+
},
|
|
285
|
+
createStandardFormModule,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
globalThis.SY_FORM_RUNTIME_V2 = formRuntime;
|
|
289
|
+
|
|
290
|
+
export default formRuntime;
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function createFormRuntimeProxyPlugin() {
|
|
295
|
+
return {
|
|
296
|
+
name: "sy-form-runtime-proxy",
|
|
297
|
+
enforce: "pre",
|
|
298
|
+
resolveId(source) {
|
|
299
|
+
if (source === "sy-form-components") return "\0sy-form-runtime-proxy:sy-form-components";
|
|
300
|
+
return null;
|
|
301
|
+
},
|
|
302
|
+
load(id) {
|
|
303
|
+
if (id !== "\0sy-form-runtime-proxy:sy-form-components") return null;
|
|
304
|
+
return `const runtime = globalThis.SY_FORM_RUNTIME_V2;
|
|
305
|
+
if (!runtime || runtime.protocol !== 'sy-form-runtime' || runtime.majorVersion !== 2) {
|
|
306
|
+
throw new Error('表单共享运行时未加载或版本不兼容');
|
|
307
|
+
}
|
|
308
|
+
const moduleValue = runtime.modules['sy-form-components'];
|
|
309
|
+
export const defineFormSchema = moduleValue.defineFormSchema;
|
|
310
|
+
export default moduleValue;
|
|
311
|
+
`;
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function generateRuntimeEntryFile(entryContent) {
|
|
317
|
+
const entryPath = path.join(tmpDir, "form-runtime-entry.jsx");
|
|
318
|
+
fs.writeFileSync(entryPath, entryContent, "utf-8");
|
|
319
|
+
return entryPath;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function generateSelfContainedEntryFile(formName) {
|
|
323
|
+
const entryContent = `import { createRoot } from 'react-dom/client';
|
|
324
|
+
import { StyleProvider } from '@ant-design/cssinjs';
|
|
325
|
+
import { App as AntdApp, ConfigProvider } from 'antd';
|
|
326
|
+
import zhCN from 'antd/locale/zh_CN';
|
|
327
|
+
import Page from '../src/forms/${formName}/page';
|
|
328
|
+
|
|
329
|
+
let root = null;
|
|
330
|
+
|
|
331
|
+
function getStyleContainer(el) {
|
|
332
|
+
const rootNode = el.getRootNode?.();
|
|
333
|
+
return rootNode && rootNode.toString?.() === '[object ShadowRoot]'
|
|
334
|
+
? rootNode
|
|
335
|
+
: document.head;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function mount(el, context = {}) {
|
|
339
|
+
root = createRoot(el);
|
|
340
|
+
root.render(
|
|
341
|
+
<StyleProvider layer container={getStyleContainer(el)}>
|
|
342
|
+
<ConfigProvider locale={zhCN} theme={{ cssVar: {} }}>
|
|
343
|
+
<AntdApp>
|
|
344
|
+
<Page
|
|
345
|
+
mode={context.mode || 'submit'}
|
|
346
|
+
initialValues={context.initialValues}
|
|
347
|
+
permissions={context.permissions}
|
|
348
|
+
formUuid={context.formUuid}
|
|
349
|
+
appType={context.appType}
|
|
350
|
+
formInstanceId={context.formInstanceId}
|
|
351
|
+
onSubmit={context.onSubmit}
|
|
352
|
+
/>
|
|
353
|
+
</AntdApp>
|
|
354
|
+
</ConfigProvider>
|
|
355
|
+
</StyleProvider>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function unmount() {
|
|
360
|
+
if (root) {
|
|
361
|
+
root.unmount();
|
|
362
|
+
root = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function update(el, nextContext) {
|
|
367
|
+
unmount();
|
|
368
|
+
mount(el, nextContext);
|
|
369
|
+
}
|
|
370
|
+
`;
|
|
371
|
+
|
|
372
|
+
const entryPath = path.join(tmpDir, `${formName}-entry.jsx`);
|
|
373
|
+
fs.writeFileSync(entryPath, entryContent, "utf-8");
|
|
374
|
+
return entryPath;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function generateSharedEntryFile(formName) {
|
|
378
|
+
const entryContent = `import schema from '../src/forms/${formName}/schema';
|
|
379
|
+
|
|
380
|
+
const runtime = globalThis.SY_FORM_RUNTIME_V2;
|
|
381
|
+
|
|
382
|
+
if (!runtime || runtime.protocol !== 'sy-form-runtime' || runtime.majorVersion !== 2) {
|
|
383
|
+
throw new Error('表单共享运行时未加载或版本不兼容');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const page = runtime.createStandardFormModule(schema);
|
|
387
|
+
|
|
388
|
+
export const mount = page.mount;
|
|
389
|
+
export const update = page.update;
|
|
390
|
+
export const unmount = page.unmount;
|
|
391
|
+
|
|
392
|
+
export default page;
|
|
393
|
+
`;
|
|
394
|
+
|
|
395
|
+
const entryPath = path.join(tmpDir, `${formName}-shared-entry.js`);
|
|
396
|
+
fs.writeFileSync(entryPath, entryContent, "utf-8");
|
|
397
|
+
return entryPath;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function readRuntimeManifest() {
|
|
401
|
+
const manifestPath = path.join(runtimeDistDir, "manifest.json");
|
|
402
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
403
|
+
try {
|
|
404
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function readRuntimeBuildCache() {
|
|
411
|
+
const cachePath = path.join(runtimeDistDir, runtimeCacheFileName);
|
|
412
|
+
if (!fs.existsSync(cachePath)) return null;
|
|
413
|
+
try {
|
|
414
|
+
return JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
415
|
+
} catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isRuntimeCacheValid(inputHash) {
|
|
421
|
+
const manifest = readRuntimeManifest();
|
|
422
|
+
const cache = readRuntimeBuildCache();
|
|
423
|
+
if (
|
|
424
|
+
!manifest ||
|
|
425
|
+
manifest.protocol !== "sy-form-runtime" ||
|
|
426
|
+
manifest.majorVersion !== 2 ||
|
|
427
|
+
!manifest.version ||
|
|
428
|
+
!manifest.files?.entry ||
|
|
429
|
+
!cache ||
|
|
430
|
+
cache.inputHash !== inputHash ||
|
|
431
|
+
cache.runtimeVersion !== manifest.version
|
|
432
|
+
) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const runtimeJsPath = path.join(runtimeDistDir, manifest.files.entry);
|
|
437
|
+
if (!fs.existsSync(runtimeJsPath)) return false;
|
|
438
|
+
if (manifest.files.css && !fs.existsSync(path.join(runtimeDistDir, manifest.files.css))) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function logRuntimeAssetSummary(prefix = "共享 runtime") {
|
|
445
|
+
const manifest = readRuntimeManifest();
|
|
446
|
+
if (!manifest) return;
|
|
447
|
+
const runtimeJsPath = path.join(runtimeDistDir, manifest.files?.entry || "runtime.js");
|
|
448
|
+
const runtimeCssPath = path.join(runtimeDistDir, manifest.files?.css || "style.css");
|
|
449
|
+
console.log(`[build] ${prefix} version: ${manifest.version}`);
|
|
450
|
+
console.log(`[build] runtime.js: ${fileSizeLabel(runtimeJsPath)} / gzip ${fileGzipSizeLabel(runtimeJsPath)}`);
|
|
451
|
+
console.log(`[build] style.css: ${fileSizeLabel(runtimeCssPath)} / gzip ${fileGzipSizeLabel(runtimeCssPath)}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function buildSharedRuntime(options = {}) {
|
|
455
|
+
const startedAt = Date.now();
|
|
456
|
+
const entryContent = createRuntimeEntryContent();
|
|
457
|
+
const inputHash = createRuntimeInputHash(entryContent);
|
|
458
|
+
if (options.runtimeCache !== false && isRuntimeCacheValid(inputHash)) {
|
|
459
|
+
console.log("[build] 表单共享 runtime 缓存命中,跳过构建");
|
|
460
|
+
logRuntimeAssetSummary("缓存 runtime");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const entryPath = generateRuntimeEntryFile(entryContent);
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
console.log("[build] 构建表单共享 runtime");
|
|
468
|
+
const cssOptions = await createCssOptions();
|
|
469
|
+
const reactPlugin = await createReactPlugin();
|
|
470
|
+
await build({
|
|
471
|
+
configFile: false,
|
|
472
|
+
root: rootDir,
|
|
473
|
+
publicDir: false,
|
|
474
|
+
css: cssOptions,
|
|
475
|
+
define: {
|
|
476
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
477
|
+
"process.env": JSON.stringify({ NODE_ENV: "production" }),
|
|
478
|
+
},
|
|
479
|
+
resolve: {
|
|
480
|
+
alias: [
|
|
481
|
+
{
|
|
482
|
+
find: "@",
|
|
483
|
+
replacement: path.join(rootDir, "src"),
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
build: {
|
|
488
|
+
target: "es2018",
|
|
489
|
+
assetsInlineLimit: 0,
|
|
490
|
+
outDir: runtimeDistDir,
|
|
491
|
+
emptyOutDir: true,
|
|
492
|
+
cssCodeSplit: false,
|
|
493
|
+
minify: true,
|
|
494
|
+
lib: {
|
|
495
|
+
entry: entryPath,
|
|
496
|
+
formats: ["es"],
|
|
497
|
+
fileName: () => "runtime.js",
|
|
498
|
+
},
|
|
499
|
+
rollupOptions: {
|
|
500
|
+
output: {
|
|
501
|
+
inlineDynamicImports: true,
|
|
502
|
+
entryFileNames: "runtime.js",
|
|
503
|
+
chunkFileNames: "runtime.js",
|
|
504
|
+
assetFileNames: (assetInfo) => {
|
|
505
|
+
if (String(assetInfo.name || "").endsWith(".css")) {
|
|
506
|
+
return "style.css";
|
|
507
|
+
}
|
|
508
|
+
return "[name][extname]";
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
plugins: [reactPlugin],
|
|
514
|
+
logLevel: "warn",
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const runtimeJsPath = path.join(runtimeDistDir, "runtime.js");
|
|
518
|
+
const runtimeCssPath = path.join(runtimeDistDir, "style.css");
|
|
519
|
+
const runtimeJs = fs.readFileSync(runtimeJsPath);
|
|
520
|
+
const runtimeCss = fs.existsSync(runtimeCssPath)
|
|
521
|
+
? fs.readFileSync(runtimeCssPath)
|
|
522
|
+
: Buffer.alloc(0);
|
|
523
|
+
const hash = crypto
|
|
524
|
+
.createHash("sha256")
|
|
525
|
+
.update(runtimeJs)
|
|
526
|
+
.update(runtimeCss)
|
|
527
|
+
.digest("hex")
|
|
528
|
+
.slice(0, 12);
|
|
529
|
+
const runtimeVersion = `v2-${hash}`;
|
|
530
|
+
fs.writeFileSync(
|
|
531
|
+
runtimeJsPath,
|
|
532
|
+
runtimeJs
|
|
533
|
+
.toString("utf-8")
|
|
534
|
+
.split(runtimeVersionPlaceholder)
|
|
535
|
+
.join(runtimeVersion),
|
|
536
|
+
"utf-8",
|
|
537
|
+
);
|
|
538
|
+
const manifest = {
|
|
539
|
+
protocol: "sy-form-runtime",
|
|
540
|
+
majorVersion: 2,
|
|
541
|
+
version: runtimeVersion,
|
|
542
|
+
inputHash,
|
|
543
|
+
files: {
|
|
544
|
+
entry: "runtime.js",
|
|
545
|
+
css: fs.existsSync(runtimeCssPath) ? "style.css" : null,
|
|
546
|
+
},
|
|
547
|
+
sizes: {
|
|
548
|
+
entry: fs.statSync(runtimeJsPath).size,
|
|
549
|
+
entryGzip: gzipSync(fs.readFileSync(runtimeJsPath)).length,
|
|
550
|
+
css: fs.existsSync(runtimeCssPath) ? fs.statSync(runtimeCssPath).size : 0,
|
|
551
|
+
cssGzip: fs.existsSync(runtimeCssPath)
|
|
552
|
+
? gzipSync(fs.readFileSync(runtimeCssPath)).length
|
|
553
|
+
: 0,
|
|
554
|
+
},
|
|
555
|
+
builtAt: new Date().toISOString(),
|
|
556
|
+
};
|
|
557
|
+
fs.writeFileSync(
|
|
558
|
+
path.join(runtimeDistDir, "manifest.json"),
|
|
559
|
+
JSON.stringify(manifest, null, 2),
|
|
560
|
+
"utf-8",
|
|
561
|
+
);
|
|
562
|
+
fs.writeFileSync(
|
|
563
|
+
path.join(runtimeDistDir, runtimeCacheFileName),
|
|
564
|
+
JSON.stringify(
|
|
565
|
+
{
|
|
566
|
+
inputHash,
|
|
567
|
+
runtimeVersion,
|
|
568
|
+
updatedAt: new Date().toISOString(),
|
|
569
|
+
},
|
|
570
|
+
null,
|
|
571
|
+
2,
|
|
572
|
+
),
|
|
573
|
+
"utf-8",
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
console.log(`[build] 表单共享 runtime 构建完成,用时 ${formatDuration(startedAt)}`);
|
|
577
|
+
logRuntimeAssetSummary();
|
|
578
|
+
} finally {
|
|
579
|
+
if (fs.existsSync(entryPath)) {
|
|
580
|
+
fs.unlinkSync(entryPath);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function buildForm(form, requestedBundleMode) {
|
|
586
|
+
const startedAt = Date.now();
|
|
587
|
+
const hasSchema = fs.existsSync(form.schemaPath);
|
|
588
|
+
const bundleMode =
|
|
589
|
+
requestedBundleMode === "shared" && hasSchema ? "shared" : "self-contained";
|
|
590
|
+
if (requestedBundleMode === "shared" && !hasSchema) {
|
|
591
|
+
console.log(`[build] ${form.name}: 无 schema.ts,使用 self-contained 构建`);
|
|
592
|
+
}
|
|
593
|
+
const entryPath =
|
|
594
|
+
bundleMode === "shared"
|
|
595
|
+
? generateSharedEntryFile(form.name)
|
|
596
|
+
: generateSelfContainedEntryFile(form.name);
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
console.log(`[build] 构建表单: ${form.name} (${bundleMode})`);
|
|
600
|
+
const cssOptions = await createCssOptions();
|
|
601
|
+
const reactPlugin = await createReactPlugin();
|
|
602
|
+
await build({
|
|
603
|
+
configFile: false,
|
|
604
|
+
root: rootDir,
|
|
605
|
+
publicDir: false,
|
|
606
|
+
css: cssOptions,
|
|
607
|
+
define: {
|
|
608
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
609
|
+
"process.env": JSON.stringify({ NODE_ENV: "production" }),
|
|
610
|
+
},
|
|
611
|
+
resolve: {
|
|
612
|
+
alias: [
|
|
613
|
+
{
|
|
614
|
+
find: "@",
|
|
615
|
+
replacement: path.join(rootDir, "src"),
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
},
|
|
619
|
+
build: {
|
|
620
|
+
target: "es2018",
|
|
621
|
+
assetsInlineLimit: 0,
|
|
622
|
+
outDir: form.outDir,
|
|
623
|
+
emptyOutDir: true,
|
|
624
|
+
lib: {
|
|
625
|
+
entry: entryPath,
|
|
626
|
+
name: form.name,
|
|
627
|
+
formats: ["es"],
|
|
628
|
+
fileName: () => "index.js",
|
|
629
|
+
},
|
|
630
|
+
cssCodeSplit: false,
|
|
631
|
+
minify: true,
|
|
632
|
+
rollupOptions: {
|
|
633
|
+
output: {
|
|
634
|
+
inlineDynamicImports: true,
|
|
635
|
+
entryFileNames: "index.js",
|
|
636
|
+
chunkFileNames: "index.js",
|
|
637
|
+
assetFileNames: (assetInfo) => {
|
|
638
|
+
if (String(assetInfo.name || "").endsWith(".css")) {
|
|
639
|
+
return "style.css";
|
|
640
|
+
}
|
|
641
|
+
return "assets/[name]-[hash][extname]";
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
plugins: [
|
|
647
|
+
bundleMode === "shared" ? createFormRuntimeProxyPlugin() : null,
|
|
648
|
+
reactPlugin,
|
|
649
|
+
].filter(Boolean),
|
|
650
|
+
logLevel: "warn",
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const entryOutput = path.join(form.outDir, "index.js");
|
|
654
|
+
const cssOutput = path.join(form.outDir, "style.css");
|
|
655
|
+
if (!fs.existsSync(cssOutput)) {
|
|
656
|
+
fs.writeFileSync(cssOutput, "", "utf-8");
|
|
657
|
+
}
|
|
658
|
+
console.log(`[build] 输出: index.js ${fileSizeLabel(entryOutput)} / gzip ${fileGzipSizeLabel(entryOutput)}`);
|
|
659
|
+
console.log(`[build] 输出: style.css ${fileSizeLabel(cssOutput)} / gzip ${fileGzipSizeLabel(cssOutput)}`);
|
|
660
|
+
const outputCode = fs.readFileSync(entryOutput, "utf-8");
|
|
661
|
+
const bareImportMatch = outputCode.match(
|
|
662
|
+
/\bfrom\s*["'](?:react|react-dom|react\/jsx-runtime|antd|sy-form-components|antd-mobile|@tiptap\/[^"']+)["']/,
|
|
663
|
+
);
|
|
664
|
+
if (bareImportMatch) {
|
|
665
|
+
throw new Error(
|
|
666
|
+
`构建产物仍包含浏览器无法直接加载的裸模块导入: ${bareImportMatch[0]}`,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (
|
|
670
|
+
bundleMode === "shared" &&
|
|
671
|
+
/(?:react-dom\/client|antd-mobile|@tiptap\/)/.test(
|
|
672
|
+
outputCode,
|
|
673
|
+
)
|
|
674
|
+
) {
|
|
675
|
+
throw new Error("shared 表单产物仍包含运行时依赖代码,请检查入口生成逻辑");
|
|
676
|
+
}
|
|
677
|
+
if (/\bprocess\.env\b/.test(outputCode)) {
|
|
678
|
+
throw new Error("构建产物仍包含浏览器不存在的 process.env 引用");
|
|
679
|
+
}
|
|
680
|
+
console.log(`[build] 表单 ${form.name} 构建完成,用时 ${formatDuration(startedAt)}`);
|
|
681
|
+
} finally {
|
|
682
|
+
if (fs.existsSync(entryPath)) {
|
|
683
|
+
fs.unlinkSync(entryPath);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function main() {
|
|
689
|
+
const args = parseArgs(process.argv.slice(2));
|
|
690
|
+
|
|
691
|
+
if (args.help) {
|
|
692
|
+
printHelp();
|
|
693
|
+
process.exit(0);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const startedAt = Date.now();
|
|
697
|
+
console.log("[build] 构建表单页面");
|
|
698
|
+
console.log(`[build] 构建模式: ${args.bundleMode}`);
|
|
699
|
+
const forms = discoverForms(args.form);
|
|
700
|
+
|
|
701
|
+
if (forms.length === 0) {
|
|
702
|
+
if (args.form) {
|
|
703
|
+
console.error(`错误: 找不到表单 "${args.form}"`);
|
|
704
|
+
} else {
|
|
705
|
+
console.error("错误: 未发现任何表单页面文件");
|
|
706
|
+
}
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (!args.form && fs.existsSync(distDir)) {
|
|
711
|
+
fs.rmSync(distDir, { recursive: true, force: true });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
715
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
716
|
+
|
|
717
|
+
if (args.bundleMode === "shared") {
|
|
718
|
+
await buildSharedRuntime({ runtimeCache: args.runtimeCache });
|
|
719
|
+
console.log("");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
let succeeded = 0;
|
|
723
|
+
let failed = 0;
|
|
724
|
+
|
|
725
|
+
for (const form of forms) {
|
|
726
|
+
try {
|
|
727
|
+
await buildForm(form, args.bundleMode);
|
|
728
|
+
succeeded += 1;
|
|
729
|
+
} catch (error) {
|
|
730
|
+
console.error(`[build] ${form.name} 构建失败: ${error.stack || error.message}`);
|
|
731
|
+
failed += 1;
|
|
732
|
+
}
|
|
733
|
+
console.log("");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
try {
|
|
737
|
+
const remaining = fs.readdirSync(tmpDir);
|
|
738
|
+
if (remaining.length === 0) {
|
|
739
|
+
fs.rmdirSync(tmpDir);
|
|
740
|
+
}
|
|
741
|
+
} catch {
|
|
742
|
+
// ignore
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
console.log(
|
|
746
|
+
`[build] 完成: ${succeeded} 成功, ${failed} 失败, 总耗时 ${formatDuration(
|
|
747
|
+
startedAt,
|
|
748
|
+
)}`,
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
if (failed > 0) {
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
await main();
|