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