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,691 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build-pages.mjs - 构建复杂代码页
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import fsSync from "node:fs";
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { gzipSync } from "node:zlib";
|
|
12
|
+
import react from "@vitejs/plugin-react";
|
|
13
|
+
import { build } from "vite";
|
|
14
|
+
import { discoverPages, distRoot, rootDir, srcRoot } from "./utils/pages.mjs";
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const tmpDir = path.join(rootDir, ".tmp");
|
|
18
|
+
const runtimeDistDir = path.join(distRoot, "page-runtime");
|
|
19
|
+
const runtimeVersionPlaceholder = "__SY_PAGE_RUNTIME_VERSION__";
|
|
20
|
+
const runtimeCacheFileName = "build-cache.json";
|
|
21
|
+
const validBundleModes = new Set(["shared", "self-contained"]);
|
|
22
|
+
|
|
23
|
+
const runtimePackages = [
|
|
24
|
+
"react",
|
|
25
|
+
"react-dom",
|
|
26
|
+
"@ant-design/cssinjs",
|
|
27
|
+
"antd",
|
|
28
|
+
"@ant-design/icons",
|
|
29
|
+
"sy-form-components",
|
|
30
|
+
"sy-page-sdk",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const proxyModuleIds = new Map(
|
|
34
|
+
[
|
|
35
|
+
"react",
|
|
36
|
+
"react/jsx-runtime",
|
|
37
|
+
"react-dom/client",
|
|
38
|
+
"@ant-design/cssinjs",
|
|
39
|
+
"antd",
|
|
40
|
+
"antd/locale/zh_CN",
|
|
41
|
+
"@ant-design/icons",
|
|
42
|
+
"sy-form-components",
|
|
43
|
+
"sy-page-sdk",
|
|
44
|
+
"sy-page-sdk/react",
|
|
45
|
+
].map((id) => [id, `\0sy-page-runtime-proxy:${id}`]),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const staticProxyExports = {
|
|
49
|
+
"sy-page-sdk": ["createPageSdk"],
|
|
50
|
+
"sy-page-sdk/react": [
|
|
51
|
+
"createReactPage",
|
|
52
|
+
"PageProvider",
|
|
53
|
+
"useCurrentUser",
|
|
54
|
+
"useDataSource",
|
|
55
|
+
"useFormViewPermissions",
|
|
56
|
+
"useMessage",
|
|
57
|
+
"useModal",
|
|
58
|
+
"useNavigation",
|
|
59
|
+
"usePageContext",
|
|
60
|
+
"usePageProps",
|
|
61
|
+
"usePageRoute",
|
|
62
|
+
"usePageSdk",
|
|
63
|
+
"createPageSdk",
|
|
64
|
+
],
|
|
65
|
+
"react/jsx-runtime": ["Fragment", "jsx", "jsxs"],
|
|
66
|
+
"react-dom/client": ["createRoot", "hydrateRoot"],
|
|
67
|
+
"@ant-design/cssinjs": ["StyleProvider", "createCache", "extractStyle"],
|
|
68
|
+
"antd/locale/zh_CN": [],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const exportNameCache = new Map();
|
|
72
|
+
|
|
73
|
+
function formatBytes(bytes) {
|
|
74
|
+
if (!Number.isFinite(bytes)) return "-";
|
|
75
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
76
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
|
77
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatDuration(startTime) {
|
|
81
|
+
return `${((Date.now() - startTime) / 1000).toFixed(2)}s`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function fileSizeLabel(filePath) {
|
|
85
|
+
if (!fsSync.existsSync(filePath)) return "未生成";
|
|
86
|
+
return formatBytes(fsSync.statSync(filePath).size);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fileGzipSizeLabel(filePath) {
|
|
90
|
+
if (!fsSync.existsSync(filePath)) return "未生成";
|
|
91
|
+
return formatBytes(gzipSync(fsSync.readFileSync(filePath)).length);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readTextIfExists(filePath) {
|
|
95
|
+
if (!fsSync.existsSync(filePath)) return "";
|
|
96
|
+
return fsSync.readFileSync(filePath, "utf-8");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readPackageVersion(packageName) {
|
|
100
|
+
try {
|
|
101
|
+
let current = path.dirname(require.resolve(packageName));
|
|
102
|
+
while (current && current !== path.dirname(current)) {
|
|
103
|
+
const packageJsonPath = path.join(current, "package.json");
|
|
104
|
+
if (fsSync.existsSync(packageJsonPath)) {
|
|
105
|
+
return JSON.parse(fsSync.readFileSync(packageJsonPath, "utf-8")).version || "unknown";
|
|
106
|
+
}
|
|
107
|
+
current = path.dirname(current);
|
|
108
|
+
}
|
|
109
|
+
return "unknown";
|
|
110
|
+
} catch {
|
|
111
|
+
return "missing";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeBundleMode(value) {
|
|
116
|
+
const mode = String(value || "shared").trim();
|
|
117
|
+
if (validBundleModes.has(mode)) return mode;
|
|
118
|
+
throw new Error(
|
|
119
|
+
`不支持的构建模式: ${mode},可选值: ${Array.from(validBundleModes).join(", ")}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string[]} argv - 命令行参数数组
|
|
125
|
+
* @returns {{ page: string, bundleMode: string, runtimeCache: boolean, help: boolean }}
|
|
126
|
+
*/
|
|
127
|
+
function parseArgs(argv) {
|
|
128
|
+
const result = {
|
|
129
|
+
page: "",
|
|
130
|
+
bundleMode: normalizeBundleMode(process.env.APP_BUNDLE_MODE),
|
|
131
|
+
runtimeCache: process.env.APP_RUNTIME_CACHE !== "false",
|
|
132
|
+
help: false,
|
|
133
|
+
};
|
|
134
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
135
|
+
const arg = argv[index];
|
|
136
|
+
if (arg === "--help" || arg === "-h") {
|
|
137
|
+
result.help = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (arg === "--page" && argv[index + 1]) {
|
|
141
|
+
result.page = argv[index + 1];
|
|
142
|
+
index += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (arg === "--bundle-mode" && argv[index + 1]) {
|
|
146
|
+
result.bundleMode = normalizeBundleMode(argv[index + 1]);
|
|
147
|
+
index += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (arg === "--no-runtime-cache") {
|
|
151
|
+
result.runtimeCache = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function printHelp() {
|
|
158
|
+
console.log(`
|
|
159
|
+
build-pages - 构建复杂代码页
|
|
160
|
+
|
|
161
|
+
用法:
|
|
162
|
+
tsx scripts/build-pages.mjs [options]
|
|
163
|
+
|
|
164
|
+
选项:
|
|
165
|
+
--page <name> 只构建指定页面目录
|
|
166
|
+
--bundle-mode shared 或 self-contained,默认 shared
|
|
167
|
+
--no-runtime-cache 强制重建共享 runtime
|
|
168
|
+
--help, -h 显示帮助信息
|
|
169
|
+
`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function createRuntimeInputHash(runtimeEntryContent) {
|
|
173
|
+
const hash = crypto.createHash("sha256");
|
|
174
|
+
const packageVersions = runtimePackages.reduce((result, packageName) => {
|
|
175
|
+
result[packageName] = readPackageVersion(packageName);
|
|
176
|
+
return result;
|
|
177
|
+
}, {});
|
|
178
|
+
const configFiles = [
|
|
179
|
+
path.join(rootDir, "src/index.css"),
|
|
180
|
+
path.join(rootDir, "tailwind.config.cjs"),
|
|
181
|
+
path.join(rootDir, "postcss.config.cjs"),
|
|
182
|
+
fileURLToPath(import.meta.url),
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
hash.update("sy-page-runtime-v1-input");
|
|
186
|
+
hash.update(runtimeEntryContent);
|
|
187
|
+
hash.update(JSON.stringify(packageVersions));
|
|
188
|
+
configFiles.forEach((filePath) => {
|
|
189
|
+
hash.update(filePath);
|
|
190
|
+
hash.update(readTextIfExists(filePath));
|
|
191
|
+
});
|
|
192
|
+
return hash.digest("hex");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function createRuntimeEntryContent() {
|
|
196
|
+
return `import * as ReactModule from 'react';
|
|
197
|
+
import * as ReactJsxRuntimeModule from 'react/jsx-runtime';
|
|
198
|
+
import * as ReactDomClientModule from 'react-dom/client';
|
|
199
|
+
import * as CssInJsModule from '@ant-design/cssinjs';
|
|
200
|
+
import * as AntdModule from 'antd';
|
|
201
|
+
import zhCN from 'antd/locale/zh_CN';
|
|
202
|
+
import * as IconsModule from '@ant-design/icons';
|
|
203
|
+
import * as SyFormComponentsModule from 'sy-form-components';
|
|
204
|
+
import * as PageSdkModule from 'sy-page-sdk';
|
|
205
|
+
import * as PageSdkReactModule from 'sy-page-sdk/react';
|
|
206
|
+
import '../src/index.css';
|
|
207
|
+
|
|
208
|
+
const runtimeVersion = '${runtimeVersionPlaceholder}';
|
|
209
|
+
|
|
210
|
+
const withDefault = (moduleValue, defaultValue) => ({
|
|
211
|
+
...moduleValue,
|
|
212
|
+
default: defaultValue ?? moduleValue.default ?? moduleValue,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
export const pageRuntime = {
|
|
216
|
+
protocol: 'sy-page-runtime',
|
|
217
|
+
majorVersion: 1,
|
|
218
|
+
version: runtimeVersion,
|
|
219
|
+
modules: {
|
|
220
|
+
react: withDefault(ReactModule, ReactModule.default || ReactModule),
|
|
221
|
+
'react/jsx-runtime': ReactJsxRuntimeModule,
|
|
222
|
+
'react-dom/client': ReactDomClientModule,
|
|
223
|
+
'@ant-design/cssinjs': CssInJsModule,
|
|
224
|
+
antd: withDefault(AntdModule, null),
|
|
225
|
+
'antd/locale/zh_CN': { default: zhCN },
|
|
226
|
+
'@ant-design/icons': IconsModule,
|
|
227
|
+
'sy-form-components': SyFormComponentsModule,
|
|
228
|
+
'sy-page-sdk': PageSdkModule,
|
|
229
|
+
'sy-page-sdk/react': PageSdkReactModule,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
globalThis.SY_PAGE_RUNTIME_V1 = pageRuntime;
|
|
234
|
+
|
|
235
|
+
export default pageRuntime;
|
|
236
|
+
`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function readRuntimeManifest() {
|
|
240
|
+
const manifestPath = path.join(runtimeDistDir, "manifest.json");
|
|
241
|
+
if (!fsSync.existsSync(manifestPath)) return null;
|
|
242
|
+
try {
|
|
243
|
+
return JSON.parse(fsSync.readFileSync(manifestPath, "utf-8"));
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function readRuntimeBuildCache() {
|
|
250
|
+
const cachePath = path.join(runtimeDistDir, runtimeCacheFileName);
|
|
251
|
+
if (!fsSync.existsSync(cachePath)) return null;
|
|
252
|
+
try {
|
|
253
|
+
return JSON.parse(fsSync.readFileSync(cachePath, "utf-8"));
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function isRuntimeCacheValid(inputHash) {
|
|
260
|
+
const manifest = readRuntimeManifest();
|
|
261
|
+
const cache = readRuntimeBuildCache();
|
|
262
|
+
if (
|
|
263
|
+
!manifest ||
|
|
264
|
+
manifest.protocol !== "sy-page-runtime" ||
|
|
265
|
+
manifest.majorVersion !== 1 ||
|
|
266
|
+
!manifest.version ||
|
|
267
|
+
!manifest.files?.entry ||
|
|
268
|
+
!cache ||
|
|
269
|
+
cache.inputHash !== inputHash ||
|
|
270
|
+
cache.runtimeVersion !== manifest.version
|
|
271
|
+
) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
const runtimeJsPath = path.join(runtimeDistDir, manifest.files.entry);
|
|
275
|
+
if (!fsSync.existsSync(runtimeJsPath)) return false;
|
|
276
|
+
if (manifest.files.css && !fsSync.existsSync(path.join(runtimeDistDir, manifest.files.css))) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function createCssOptions() {
|
|
283
|
+
const tailwindcssModule = await import("tailwindcss");
|
|
284
|
+
const autoprefixerModule = await import("autoprefixer");
|
|
285
|
+
const tailwindcss = tailwindcssModule.default ?? tailwindcssModule;
|
|
286
|
+
const autoprefixer = autoprefixerModule.default ?? autoprefixerModule;
|
|
287
|
+
return {
|
|
288
|
+
postcss: {
|
|
289
|
+
plugins: [
|
|
290
|
+
tailwindcss({ config: path.join(rootDir, "tailwind.config.cjs") }),
|
|
291
|
+
autoprefixer(),
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function createResolveAlias() {
|
|
298
|
+
return [
|
|
299
|
+
{
|
|
300
|
+
find: "@",
|
|
301
|
+
replacement: srcRoot,
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function buildSharedRuntime(options = {}) {
|
|
307
|
+
const startedAt = Date.now();
|
|
308
|
+
const entryContent = createRuntimeEntryContent();
|
|
309
|
+
const inputHash = createRuntimeInputHash(entryContent);
|
|
310
|
+
if (options.runtimeCache !== false && isRuntimeCacheValid(inputHash)) {
|
|
311
|
+
const manifest = readRuntimeManifest();
|
|
312
|
+
console.log(`[build] 代码页共享 runtime 缓存命中: ${manifest.version}`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const entryPath = path.join(tmpDir, "page-runtime-entry.jsx");
|
|
317
|
+
await fs.writeFile(entryPath, entryContent, "utf8");
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const cssOptions = await createCssOptions();
|
|
321
|
+
console.log("[build] 构建代码页共享 runtime");
|
|
322
|
+
await build({
|
|
323
|
+
configFile: false,
|
|
324
|
+
root: rootDir,
|
|
325
|
+
publicDir: false,
|
|
326
|
+
css: cssOptions,
|
|
327
|
+
define: {
|
|
328
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
329
|
+
"process.env": JSON.stringify({ NODE_ENV: "production" }),
|
|
330
|
+
},
|
|
331
|
+
plugins: [react()],
|
|
332
|
+
resolve: {
|
|
333
|
+
alias: createResolveAlias(),
|
|
334
|
+
dedupe: ["react", "react-dom", "antd", "@ant-design/cssinjs"],
|
|
335
|
+
},
|
|
336
|
+
build: {
|
|
337
|
+
target: "es2018",
|
|
338
|
+
assetsInlineLimit: 0,
|
|
339
|
+
cssCodeSplit: false,
|
|
340
|
+
emptyOutDir: true,
|
|
341
|
+
minify: true,
|
|
342
|
+
outDir: runtimeDistDir,
|
|
343
|
+
lib: {
|
|
344
|
+
entry: entryPath,
|
|
345
|
+
formats: ["es"],
|
|
346
|
+
fileName: () => "runtime.js",
|
|
347
|
+
},
|
|
348
|
+
rollupOptions: {
|
|
349
|
+
output: {
|
|
350
|
+
inlineDynamicImports: true,
|
|
351
|
+
entryFileNames: "runtime.js",
|
|
352
|
+
chunkFileNames: "runtime.js",
|
|
353
|
+
assetFileNames: (assetInfo) => {
|
|
354
|
+
if (String(assetInfo.name || "").endsWith(".css")) {
|
|
355
|
+
return "style.css";
|
|
356
|
+
}
|
|
357
|
+
return "assets/[name]-[hash][extname]";
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
logLevel: "warn",
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const runtimeJsPath = path.join(runtimeDistDir, "runtime.js");
|
|
366
|
+
const runtimeCssPath = path.join(runtimeDistDir, "style.css");
|
|
367
|
+
const runtimeJs = fsSync.readFileSync(runtimeJsPath);
|
|
368
|
+
const runtimeCss = fsSync.existsSync(runtimeCssPath)
|
|
369
|
+
? fsSync.readFileSync(runtimeCssPath)
|
|
370
|
+
: Buffer.alloc(0);
|
|
371
|
+
const hash = crypto
|
|
372
|
+
.createHash("sha256")
|
|
373
|
+
.update(runtimeJs)
|
|
374
|
+
.update(runtimeCss)
|
|
375
|
+
.digest("hex")
|
|
376
|
+
.slice(0, 12);
|
|
377
|
+
const runtimeVersion = `v1-${hash}`;
|
|
378
|
+
await fs.writeFile(
|
|
379
|
+
runtimeJsPath,
|
|
380
|
+
runtimeJs
|
|
381
|
+
.toString("utf-8")
|
|
382
|
+
.split(runtimeVersionPlaceholder)
|
|
383
|
+
.join(runtimeVersion),
|
|
384
|
+
"utf8",
|
|
385
|
+
);
|
|
386
|
+
const manifest = {
|
|
387
|
+
protocol: "sy-page-runtime",
|
|
388
|
+
majorVersion: 1,
|
|
389
|
+
version: runtimeVersion,
|
|
390
|
+
inputHash,
|
|
391
|
+
files: {
|
|
392
|
+
entry: "runtime.js",
|
|
393
|
+
css: fsSync.existsSync(runtimeCssPath) ? "style.css" : null,
|
|
394
|
+
},
|
|
395
|
+
sizes: {
|
|
396
|
+
entry: fsSync.statSync(runtimeJsPath).size,
|
|
397
|
+
entryGzip: gzipSync(fsSync.readFileSync(runtimeJsPath)).length,
|
|
398
|
+
css: fsSync.existsSync(runtimeCssPath) ? fsSync.statSync(runtimeCssPath).size : 0,
|
|
399
|
+
cssGzip: fsSync.existsSync(runtimeCssPath)
|
|
400
|
+
? gzipSync(fsSync.readFileSync(runtimeCssPath)).length
|
|
401
|
+
: 0,
|
|
402
|
+
},
|
|
403
|
+
builtAt: new Date().toISOString(),
|
|
404
|
+
};
|
|
405
|
+
await fs.writeFile(
|
|
406
|
+
path.join(runtimeDistDir, "manifest.json"),
|
|
407
|
+
JSON.stringify(manifest, null, 2),
|
|
408
|
+
"utf8",
|
|
409
|
+
);
|
|
410
|
+
await fs.writeFile(
|
|
411
|
+
path.join(runtimeDistDir, runtimeCacheFileName),
|
|
412
|
+
JSON.stringify(
|
|
413
|
+
{
|
|
414
|
+
inputHash,
|
|
415
|
+
runtimeVersion,
|
|
416
|
+
updatedAt: new Date().toISOString(),
|
|
417
|
+
},
|
|
418
|
+
null,
|
|
419
|
+
2,
|
|
420
|
+
),
|
|
421
|
+
"utf8",
|
|
422
|
+
);
|
|
423
|
+
console.log(`[build] 代码页共享 runtime 构建完成,用时 ${formatDuration(startedAt)}`);
|
|
424
|
+
console.log(`[build] runtime.js: ${fileSizeLabel(runtimeJsPath)} / gzip ${fileGzipSizeLabel(runtimeJsPath)}`);
|
|
425
|
+
console.log(`[build] style.css: ${fileSizeLabel(runtimeCssPath)} / gzip ${fileGzipSizeLabel(runtimeCssPath)}`);
|
|
426
|
+
} finally {
|
|
427
|
+
await fs.rm(entryPath, { force: true });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function isValidExportName(name) {
|
|
432
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) && name !== "default";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function getProxyExportNames(source) {
|
|
436
|
+
if (exportNameCache.has(source)) return exportNameCache.get(source);
|
|
437
|
+
if (staticProxyExports[source]) {
|
|
438
|
+
exportNameCache.set(source, staticProxyExports[source]);
|
|
439
|
+
return staticProxyExports[source];
|
|
440
|
+
}
|
|
441
|
+
if (source === "sy-form-components" || source === "@ant-design/icons") {
|
|
442
|
+
const parsed = readPackageTypeExports(source);
|
|
443
|
+
exportNameCache.set(source, parsed);
|
|
444
|
+
return parsed;
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
const moduleValue = await import(source);
|
|
448
|
+
const names = Object.keys(moduleValue).filter(isValidExportName);
|
|
449
|
+
exportNameCache.set(source, names);
|
|
450
|
+
return names;
|
|
451
|
+
} catch {
|
|
452
|
+
exportNameCache.set(source, []);
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function readPackageTypeExports(source) {
|
|
458
|
+
try {
|
|
459
|
+
const entryPath = require.resolve(source);
|
|
460
|
+
let currentDir = path.dirname(entryPath);
|
|
461
|
+
let packageJsonPath = "";
|
|
462
|
+
while (currentDir && currentDir !== path.dirname(currentDir)) {
|
|
463
|
+
const candidate = path.join(currentDir, "package.json");
|
|
464
|
+
if (fsSync.existsSync(candidate)) {
|
|
465
|
+
packageJsonPath = candidate;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
currentDir = path.dirname(currentDir);
|
|
469
|
+
}
|
|
470
|
+
if (!packageJsonPath) return [];
|
|
471
|
+
const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, "utf-8"));
|
|
472
|
+
const typesPath = packageJson.types || packageJson.typings;
|
|
473
|
+
if (!typesPath) return [];
|
|
474
|
+
const dtsPath = path.resolve(path.dirname(packageJsonPath), typesPath);
|
|
475
|
+
const content = fsSync.readFileSync(dtsPath, "utf-8");
|
|
476
|
+
const names = new Set();
|
|
477
|
+
for (const match of content.matchAll(
|
|
478
|
+
/export\s+(?:declare\s+)?(?:const|function|class)\s+([A-Za-z_$][\w$]*)/g,
|
|
479
|
+
)) {
|
|
480
|
+
names.add(match[1]);
|
|
481
|
+
}
|
|
482
|
+
for (const match of content.matchAll(/export\s*\{([^}]+)\}/g)) {
|
|
483
|
+
match[1]
|
|
484
|
+
.split(",")
|
|
485
|
+
.map((item) => item.trim().split(/\s+as\s+/).pop()?.trim())
|
|
486
|
+
.filter((item) => item && isValidExportName(item))
|
|
487
|
+
.forEach((item) => names.add(item));
|
|
488
|
+
}
|
|
489
|
+
return Array.from(names);
|
|
490
|
+
} catch {
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function createProxyModuleCode(source) {
|
|
496
|
+
const exportNames = await getProxyExportNames(source);
|
|
497
|
+
const globalKey = JSON.stringify(source);
|
|
498
|
+
const namedExports = exportNames
|
|
499
|
+
.map((name) => `export const ${name} = moduleValue[${JSON.stringify(name)}];`)
|
|
500
|
+
.join("\n");
|
|
501
|
+
const defaultExport =
|
|
502
|
+
source === "antd/locale/zh_CN"
|
|
503
|
+
? "export default moduleValue.default;"
|
|
504
|
+
: `export default (moduleValue.default ?? moduleValue);`;
|
|
505
|
+
|
|
506
|
+
return `const runtime = globalThis.SY_PAGE_RUNTIME_V1;
|
|
507
|
+
if (!runtime || runtime.protocol !== 'sy-page-runtime' || runtime.majorVersion !== 1) {
|
|
508
|
+
throw new Error('代码页共享运行时未加载或版本不兼容');
|
|
509
|
+
}
|
|
510
|
+
const moduleValue = runtime.modules[${globalKey}];
|
|
511
|
+
if (!moduleValue) {
|
|
512
|
+
throw new Error('代码页共享运行时缺少模块: ${source}');
|
|
513
|
+
}
|
|
514
|
+
${namedExports}
|
|
515
|
+
${defaultExport}
|
|
516
|
+
`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function createSharedRuntimeProxyPlugin() {
|
|
520
|
+
return {
|
|
521
|
+
name: "sy-page-runtime-proxy",
|
|
522
|
+
enforce: "pre",
|
|
523
|
+
resolveId(source) {
|
|
524
|
+
return proxyModuleIds.get(source) || null;
|
|
525
|
+
},
|
|
526
|
+
async load(id) {
|
|
527
|
+
const source = Array.from(proxyModuleIds.entries()).find(
|
|
528
|
+
([, proxyId]) => proxyId === id,
|
|
529
|
+
)?.[0];
|
|
530
|
+
if (!source) return null;
|
|
531
|
+
return createProxyModuleCode(source);
|
|
532
|
+
},
|
|
533
|
+
transform(code, id) {
|
|
534
|
+
if (!id.includes(`${path.sep}src${path.sep}pages${path.sep}`)) return null;
|
|
535
|
+
if (!/\.[cm]?[jt]sx?$/.test(id)) return null;
|
|
536
|
+
return code.replace(
|
|
537
|
+
/^\s*import\s+["'](?:\.\.\/\.\.\/index\.css|\.\.\/index\.css)["'];?\s*$/gm,
|
|
538
|
+
"",
|
|
539
|
+
);
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* 确保构建产物的文件名与预期一致,必要时重命名
|
|
546
|
+
* @param {string} targetDir - 目标目录
|
|
547
|
+
* @param {string} expectedName - 期望的文件名
|
|
548
|
+
* @param {string} extension - 文件扩展名
|
|
549
|
+
* @returns {Promise<string|null>}
|
|
550
|
+
*/
|
|
551
|
+
async function ensureFileName(targetDir, expectedName, extension) {
|
|
552
|
+
const files = await fs.readdir(targetDir);
|
|
553
|
+
const matchingFile = files.find((fileName) => fileName.endsWith(extension));
|
|
554
|
+
if (!matchingFile) return null;
|
|
555
|
+
if (matchingFile === expectedName) return path.join(targetDir, matchingFile);
|
|
556
|
+
const sourcePath = path.join(targetDir, matchingFile);
|
|
557
|
+
const targetPath = path.join(targetDir, expectedName);
|
|
558
|
+
await fs.rename(sourcePath, targetPath);
|
|
559
|
+
return targetPath;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* 构建单个代码页的 Vite bundle
|
|
564
|
+
* @param {{ config: { code: string }, entryPath: string }} page - 页面信息
|
|
565
|
+
* @param {string} bundleMode - 构建模式
|
|
566
|
+
* @returns {Promise<void>}
|
|
567
|
+
*/
|
|
568
|
+
async function buildPage(page, bundleMode) {
|
|
569
|
+
const startedAt = Date.now();
|
|
570
|
+
console.log(`📦 构建代码页: ${page.config.code} (${bundleMode})`);
|
|
571
|
+
const outDir = path.join(distRoot, "pages", page.config.code);
|
|
572
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
573
|
+
|
|
574
|
+
const cssOptions = await createCssOptions();
|
|
575
|
+
await build({
|
|
576
|
+
configFile: false,
|
|
577
|
+
mode: "production",
|
|
578
|
+
root: rootDir,
|
|
579
|
+
publicDir: false,
|
|
580
|
+
css: cssOptions,
|
|
581
|
+
define: {
|
|
582
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
583
|
+
"process.env": JSON.stringify({ NODE_ENV: "production" }),
|
|
584
|
+
},
|
|
585
|
+
plugins: [bundleMode === "shared" ? createSharedRuntimeProxyPlugin() : null, react()].filter(
|
|
586
|
+
Boolean,
|
|
587
|
+
),
|
|
588
|
+
resolve: {
|
|
589
|
+
alias: createResolveAlias(),
|
|
590
|
+
dedupe: ["react", "react-dom", "antd", "@ant-design/cssinjs"],
|
|
591
|
+
},
|
|
592
|
+
build: {
|
|
593
|
+
target: "es2018",
|
|
594
|
+
assetsInlineLimit: 0,
|
|
595
|
+
cssCodeSplit: false,
|
|
596
|
+
emptyOutDir: true,
|
|
597
|
+
minify: true,
|
|
598
|
+
outDir,
|
|
599
|
+
lib: {
|
|
600
|
+
entry: page.entryPath,
|
|
601
|
+
formats: ["es"],
|
|
602
|
+
fileName: () => "index",
|
|
603
|
+
},
|
|
604
|
+
rollupOptions: {
|
|
605
|
+
output: {
|
|
606
|
+
inlineDynamicImports: true,
|
|
607
|
+
entryFileNames: "index.js",
|
|
608
|
+
chunkFileNames: "index.js",
|
|
609
|
+
assetFileNames: (assetInfo) => {
|
|
610
|
+
if (String(assetInfo.name || "").endsWith(".css")) {
|
|
611
|
+
return "style.css";
|
|
612
|
+
}
|
|
613
|
+
return "assets/[name]-[hash][extname]";
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
logLevel: "warn",
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const jsPath = await ensureFileName(outDir, "index.js", ".js");
|
|
622
|
+
if (!jsPath) throw new Error(`页面 ${page.config.code} 未生成 index.js`);
|
|
623
|
+
const cssPath = await ensureFileName(outDir, "style.css", ".css");
|
|
624
|
+
if (!cssPath) await fs.writeFile(path.join(outDir, "style.css"), "", "utf8");
|
|
625
|
+
|
|
626
|
+
const outputCode = await fs.readFile(jsPath, "utf8");
|
|
627
|
+
const bareImportMatch = outputCode.match(
|
|
628
|
+
/\bfrom\s*["'](?:react|react-dom|react\/jsx-runtime|antd|@ant-design\/cssinjs|@ant-design\/icons|sy-form-components|@sy\/page-sdk(?:\/react)?)["']/,
|
|
629
|
+
);
|
|
630
|
+
if (bareImportMatch) {
|
|
631
|
+
throw new Error(`构建产物仍包含浏览器无法直接加载的裸模块导入: ${bareImportMatch[0]}`);
|
|
632
|
+
}
|
|
633
|
+
if (/\bprocess\.env\b/.test(outputCode)) {
|
|
634
|
+
throw new Error("构建产物仍包含浏览器不存在的 process.env 引用");
|
|
635
|
+
}
|
|
636
|
+
console.log(`[build] 代码页 ${page.config.code} 构建完成,用时 ${formatDuration(startedAt)}`);
|
|
637
|
+
console.log(`[build] index.js: ${fileSizeLabel(jsPath)} / gzip ${fileGzipSizeLabel(jsPath)}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function main() {
|
|
641
|
+
const args = parseArgs(process.argv.slice(2));
|
|
642
|
+
if (args.help) {
|
|
643
|
+
printHelp();
|
|
644
|
+
process.exit(0);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const pages = await discoverPages(args.page);
|
|
648
|
+
if (pages.length === 0) {
|
|
649
|
+
if (args.page) {
|
|
650
|
+
throw new Error(`找不到代码页 "${args.page}"`);
|
|
651
|
+
}
|
|
652
|
+
console.log("未发现复杂代码页,跳过 pages 构建");
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (!args.page) {
|
|
657
|
+
await fs.rm(path.join(distRoot, "pages"), { recursive: true, force: true });
|
|
658
|
+
}
|
|
659
|
+
await fs.mkdir(tmpDir, { recursive: true });
|
|
660
|
+
|
|
661
|
+
if (args.bundleMode === "shared") {
|
|
662
|
+
await buildSharedRuntime({ runtimeCache: args.runtimeCache });
|
|
663
|
+
console.log("");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
let succeeded = 0;
|
|
667
|
+
let failed = 0;
|
|
668
|
+
for (const page of pages) {
|
|
669
|
+
try {
|
|
670
|
+
await buildPage(page, args.bundleMode);
|
|
671
|
+
succeeded += 1;
|
|
672
|
+
} catch (error) {
|
|
673
|
+
failed += 1;
|
|
674
|
+
console.error(` ❌ 构建失败: ${error.stack || error.message}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
const remaining = await fs.readdir(tmpDir);
|
|
680
|
+
if (remaining.length === 0) {
|
|
681
|
+
await fs.rmdir(tmpDir);
|
|
682
|
+
}
|
|
683
|
+
} catch {
|
|
684
|
+
// ignore
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
console.log(`完成: ${succeeded} 成功, ${failed} 失败`);
|
|
688
|
+
if (failed > 0) process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
await main();
|