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.
- package/README.md +58 -0
- package/bin/openxiangda.js +11 -0
- package/lib/cli.js +2423 -0
- package/lib/config.js +121 -0
- package/lib/http.js +47 -0
- package/lib/skills.js +371 -0
- package/lib/utils.js +87 -0
- package/lib/workspace-init.js +139 -0
- package/openxiangda-skills/SKILL.md +128 -0
- package/openxiangda-skills/references/architecture-patterns.md +242 -0
- package/openxiangda-skills/references/automation-v3.md +129 -0
- package/openxiangda-skills/references/component-guide.md +198 -0
- package/openxiangda-skills/references/forms/component-registry.md +53 -0
- package/openxiangda-skills/references/forms/form-schema.md +109 -0
- package/openxiangda-skills/references/forms/layout-and-rules.md +24 -0
- package/openxiangda-skills/references/openxiangda-api.md +466 -0
- package/openxiangda-skills/references/pages/page-sdk.md +13 -0
- package/openxiangda-skills/references/pages/publish-flow.md +36 -0
- package/openxiangda-skills/references/pages/workspace-structure.md +38 -0
- package/openxiangda-skills/references/permissions-settings.md +147 -0
- package/openxiangda-skills/references/platform-data-model.md +305 -0
- package/openxiangda-skills/references/style-system.md +492 -0
- package/openxiangda-skills/references/troubleshooting.md +246 -0
- package/openxiangda-skills/references/workflow-v3.md +105 -0
- package/openxiangda-skills/references/workspace-state.md +45 -0
- package/openxiangda-skills/skills/openxiangda-app/SKILL.md +64 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +143 -0
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +76 -0
- package/openxiangda-skills/skills/openxiangda-inspect/SKILL.md +40 -0
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +62 -0
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +95 -0
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +97 -0
- package/package.json +126 -0
- package/packages/sdk/bin/lowcode-workspace.mjs +4 -0
- package/packages/sdk/dist/build/index.cjs +33 -0
- package/packages/sdk/dist/build/index.cjs.map +1 -0
- package/packages/sdk/dist/build/index.d.mts +40 -0
- package/packages/sdk/dist/build/index.d.ts +40 -0
- package/packages/sdk/dist/build/index.mjs +8 -0
- package/packages/sdk/dist/build/index.mjs.map +1 -0
- package/packages/sdk/dist/components/index.cjs +18700 -0
- package/packages/sdk/dist/components/index.cjs.map +1 -0
- package/packages/sdk/dist/components/index.d.mts +2094 -0
- package/packages/sdk/dist/components/index.d.ts +2094 -0
- package/packages/sdk/dist/components/index.mjs +18649 -0
- package/packages/sdk/dist/components/index.mjs.map +1 -0
- package/packages/sdk/dist/runtime/index.cjs +1469 -0
- package/packages/sdk/dist/runtime/index.cjs.map +1 -0
- package/packages/sdk/dist/runtime/index.d.mts +831 -0
- package/packages/sdk/dist/runtime/index.d.ts +831 -0
- package/packages/sdk/dist/runtime/index.mjs +1420 -0
- package/packages/sdk/dist/runtime/index.mjs.map +1 -0
- package/packages/sdk/dist/styles/antd-theme.cjs +60 -0
- package/packages/sdk/dist/styles/antd-theme.cjs.map +1 -0
- package/packages/sdk/dist/styles/antd-theme.d.mts +5 -0
- package/packages/sdk/dist/styles/antd-theme.d.ts +5 -0
- package/packages/sdk/dist/styles/antd-theme.mjs +35 -0
- package/packages/sdk/dist/styles/antd-theme.mjs.map +1 -0
- package/packages/sdk/dist/styles/tailwind-preset.cjs +2641 -0
- package/packages/sdk/dist/styles/tailwind-preset.cjs.map +1 -0
- package/packages/sdk/dist/styles/tailwind-preset.d.mts +75 -0
- package/packages/sdk/dist/styles/tailwind-preset.d.ts +75 -0
- package/packages/sdk/dist/styles/tailwind-preset.mjs +2618 -0
- package/packages/sdk/dist/styles/tailwind-preset.mjs.map +1 -0
- package/packages/sdk/dist/styles/tokens.css +73 -0
- package/packages/sdk/src/build-source/README.md +9 -0
- package/packages/sdk/src/build-source/bin/lowcode-workspace.mjs +7 -0
- package/packages/sdk/src/build-source/package.json +34 -0
- package/packages/sdk/src/build-source/scripts/build-forms.mjs +824 -0
- package/packages/sdk/src/build-source/scripts/build-forms.runtime-entry.test.ts +18 -0
- package/packages/sdk/src/build-source/scripts/build-pages.mjs +793 -0
- package/packages/sdk/src/build-source/scripts/build-workspace.mjs +64 -0
- package/packages/sdk/src/build-source/scripts/publish-all.mjs +127 -0
- package/packages/sdk/src/build-source/scripts/publish-oss.mjs +149 -0
- package/packages/sdk/src/build-source/scripts/register-bundle.mjs +1 -0
- package/packages/sdk/src/build-source/scripts/register.mjs +329 -0
- package/packages/sdk/src/build-source/scripts/sync-schema.mjs +301 -0
- package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +639 -0
- package/packages/sdk/src/build-source/scripts/utils/form-api.test.ts +244 -0
- package/packages/sdk/src/build-source/scripts/utils/form-runtime-assets.mjs +57 -0
- package/packages/sdk/src/build-source/scripts/utils/form-runtime-assets.test.ts +135 -0
- package/packages/sdk/src/build-source/scripts/utils/incremental.mjs +210 -0
- package/packages/sdk/src/build-source/scripts/utils/load-config.mjs +257 -0
- package/packages/sdk/src/build-source/scripts/utils/load-config.test.ts +44 -0
- package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +70 -0
- package/packages/sdk/src/build-source/scripts/utils/namespace-css.mjs +61 -0
- package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +128 -0
- package/packages/sdk/src/build-source/scripts/utils/pages.mjs +80 -0
- package/packages/sdk/src/build-source/scripts/utils/progress.mjs +57 -0
- package/packages/sdk/src/build-source/scripts/utils/register-payload.mjs +89 -0
- package/packages/sdk/src/build-source/scripts/utils/register-payload.test.ts +76 -0
- package/packages/sdk/src/build-source/scripts/utils/runtime-css-check.mjs +44 -0
- package/packages/sdk/src/build-source/scripts/utils/runtime-css-check.test.ts +54 -0
- package/packages/sdk/src/build-source/scripts/utils/schema-transform.mjs +130 -0
- package/packages/sdk/src/build-source/scripts/utils/schema-transform.test.ts +141 -0
- package/packages/sdk/src/build-source/scripts/utils/tailwind-config.mjs +227 -0
- package/packages/sdk/src/build-source/scripts/utils/tailwind-config.test.ts +187 -0
- package/packages/sdk/src/build-source/src/cli.mjs +679 -0
- package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +34 -0
- package/templates/sy-lowcode-app-workspace/examples/forms/customer/page.tsx +1 -0
- package/templates/sy-lowcode-app-workspace/examples/forms/customer/schema.ts +35 -0
- package/templates/sy-lowcode-app-workspace/index.html +12 -0
- package/templates/sy-lowcode-app-workspace/package.json +49 -0
- package/templates/sy-lowcode-app-workspace/postcss.config.cjs +6 -0
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +100 -0
- package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +26 -0
- package/templates/sy-lowcode-app-workspace/src/forms/.gitkeep +1 -0
- package/templates/sy-lowcode-app-workspace/src/forms/README.md +48 -0
- package/templates/sy-lowcode-app-workspace/src/index.css +28 -0
- package/templates/sy-lowcode-app-workspace/src/js-code-nodes/.gitkeep +1 -0
- package/templates/sy-lowcode-app-workspace/src/js-code-nodes/types.d.ts +3 -0
- package/templates/sy-lowcode-app-workspace/src/main.tsx +36 -0
- package/templates/sy-lowcode-app-workspace/src/pages/.gitkeep +1 -0
- package/templates/sy-lowcode-app-workspace/src/shared/form-schema.ts +128 -0
- package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +31 -0
- package/templates/sy-lowcode-app-workspace/tailwind.config.cjs +30 -0
- package/templates/sy-lowcode-app-workspace/tsconfig.app.json +24 -0
- package/templates/sy-lowcode-app-workspace/tsconfig.js-code-nodes.json +15 -0
- package/templates/sy-lowcode-app-workspace/tsconfig.json +7 -0
- package/templates/sy-lowcode-app-workspace/tsconfig.node.json +10 -0
- package/templates/sy-lowcode-app-workspace/vite.config.ts +32 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { config as dotenvConfig } from "dotenv";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
|
|
5
|
+
export const rootDir = resolve(process.env.LOWCODE_WORKSPACE_ROOT || process.cwd());
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 标准化 OSS 路径前缀,去除前后斜杠
|
|
9
|
+
* @param {string|undefined} pathPrefix
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
function normalizePathPrefix(pathPrefix) {
|
|
13
|
+
return String(pathPrefix || "app-workspace").replace(/^\/+|\/+$/g, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 解析逗号分隔的字符串为数组
|
|
18
|
+
* @param {string|undefined} value
|
|
19
|
+
* @param {string} [fallback]
|
|
20
|
+
* @returns {string[]}
|
|
21
|
+
*/
|
|
22
|
+
function parseCsv(value, fallback) {
|
|
23
|
+
return String(value || fallback || "")
|
|
24
|
+
.split(",")
|
|
25
|
+
.map((item) => item.trim())
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getDefaultCorsOrigin(platformUrl) {
|
|
30
|
+
try {
|
|
31
|
+
return new URL(platformUrl).origin;
|
|
32
|
+
} catch {
|
|
33
|
+
return "*";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createBuildId() {
|
|
38
|
+
return new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeBaseUrl(value) {
|
|
42
|
+
return String(value || "").replace(/\/+$/, "");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeCssIsolation(value) {
|
|
46
|
+
if (value === "none") return "none";
|
|
47
|
+
if (value === "shadow") {
|
|
48
|
+
console.warn(
|
|
49
|
+
"[lowcode-workspace] cssIsolation='shadow' is deprecated; use 'namespace' instead.",
|
|
50
|
+
);
|
|
51
|
+
return "shadow";
|
|
52
|
+
}
|
|
53
|
+
return "namespace";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveOpenXiangdaEndpointConfig(baseUrl) {
|
|
57
|
+
const raw = normalizeBaseUrl(baseUrl);
|
|
58
|
+
if (!raw) {
|
|
59
|
+
return { platformUrl: "", servicePrefix: "" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const url = new URL(raw);
|
|
64
|
+
const cleanPath = url.pathname.replace(/\/+$/, "");
|
|
65
|
+
if (
|
|
66
|
+
!cleanPath ||
|
|
67
|
+
cleanPath === "/platform" ||
|
|
68
|
+
cleanPath.startsWith("/platform/") ||
|
|
69
|
+
cleanPath === "/view" ||
|
|
70
|
+
cleanPath.startsWith("/view/")
|
|
71
|
+
) {
|
|
72
|
+
return { platformUrl: url.origin, servicePrefix: "/service" };
|
|
73
|
+
}
|
|
74
|
+
return { platformUrl: `${url.origin}${cleanPath}`, servicePrefix: "" };
|
|
75
|
+
} catch {
|
|
76
|
+
return { platformUrl: raw, servicePrefix: "" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function loadConfigModule() {
|
|
81
|
+
const configPath = resolve(rootDir, "app-workspace.config.ts");
|
|
82
|
+
const loaded = await import(pathToFileURL(configPath).href);
|
|
83
|
+
return loaded.default || loaded;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 加载并合并应用配置(文件 + 环境变量)
|
|
88
|
+
* @returns {Promise<object>} 合并后的应用配置对象
|
|
89
|
+
*/
|
|
90
|
+
export async function loadConfig() {
|
|
91
|
+
const mode = process.env.APP_MODE || "development";
|
|
92
|
+
|
|
93
|
+
if (mode !== "development") {
|
|
94
|
+
dotenvConfig({ path: resolve(rootDir, `.env.${mode}`) });
|
|
95
|
+
}
|
|
96
|
+
dotenvConfig({ path: resolve(rootDir, ".env") });
|
|
97
|
+
|
|
98
|
+
const source = await loadConfigModule();
|
|
99
|
+
const openXiangdaMode = Boolean(
|
|
100
|
+
process.env.OPENXIANGDA_ACCESS_TOKEN || process.env.OPENXIANGDA_BASE_URL,
|
|
101
|
+
);
|
|
102
|
+
const openXiangdaEndpoint = process.env.OPENXIANGDA_BASE_URL
|
|
103
|
+
? resolveOpenXiangdaEndpointConfig(process.env.OPENXIANGDA_BASE_URL)
|
|
104
|
+
: null;
|
|
105
|
+
const platformUrl = openXiangdaEndpoint
|
|
106
|
+
? openXiangdaEndpoint.platformUrl
|
|
107
|
+
: source.platformUrl || process.env.APP_PLATFORM_URL;
|
|
108
|
+
const explicitBuildId = Boolean(source.buildId || process.env.APP_BUILD_ID);
|
|
109
|
+
|
|
110
|
+
const normalized = {
|
|
111
|
+
...source,
|
|
112
|
+
appType: process.env.OPENXIANGDA_APP_TYPE || source.appType || process.env.APP_TYPE,
|
|
113
|
+
appName: source.appName || process.env.APP_NAME || "低代码应用",
|
|
114
|
+
platformUrl,
|
|
115
|
+
servicePrefix:
|
|
116
|
+
openXiangdaEndpoint?.servicePrefix ??
|
|
117
|
+
(source.servicePrefix ||
|
|
118
|
+
process.env.APP_SERVICE_PREFIX ||
|
|
119
|
+
(openXiangdaMode ? "" : "/service")),
|
|
120
|
+
openXiangdaAccessToken: process.env.OPENXIANGDA_ACCESS_TOKEN || "",
|
|
121
|
+
openXiangdaProfile: process.env.OPENXIANGDA_PROFILE || "",
|
|
122
|
+
appKey: source.appKey || process.env.APP_KEY,
|
|
123
|
+
appSecret: source.appSecret || process.env.APP_SECRET,
|
|
124
|
+
userId: source.userId || process.env.APP_USER_ID,
|
|
125
|
+
version: source.version || process.env.APP_VERSION || "0.1.0",
|
|
126
|
+
buildId: source.buildId || process.env.APP_BUILD_ID || createBuildId(),
|
|
127
|
+
buildIdExplicit: explicitBuildId,
|
|
128
|
+
oss: {
|
|
129
|
+
...(source.oss || {}),
|
|
130
|
+
region: source.oss?.region || process.env.APP_OSS_REGION,
|
|
131
|
+
bucket: source.oss?.bucket || process.env.APP_OSS_BUCKET,
|
|
132
|
+
accessKeyId:
|
|
133
|
+
source.oss?.accessKeyId || process.env.APP_OSS_ACCESS_KEY_ID,
|
|
134
|
+
accessKeySecret:
|
|
135
|
+
source.oss?.accessKeySecret || process.env.APP_OSS_ACCESS_KEY_SECRET,
|
|
136
|
+
pathPrefix: normalizePathPrefix(
|
|
137
|
+
source.oss?.pathPrefix || process.env.APP_OSS_PATH_PREFIX,
|
|
138
|
+
),
|
|
139
|
+
corsOrigins: parseCsv(
|
|
140
|
+
process.env.APP_OSS_CORS_ORIGINS,
|
|
141
|
+
getDefaultCorsOrigin(platformUrl),
|
|
142
|
+
),
|
|
143
|
+
skipCors: process.env.APP_OSS_SKIP_CORS === "1" || source.oss?.skipCors,
|
|
144
|
+
},
|
|
145
|
+
defaults: {
|
|
146
|
+
protocolVersion:
|
|
147
|
+
source.defaults?.protocolVersion ||
|
|
148
|
+
process.env.APP_PAGE_PROTOCOL_VERSION ||
|
|
149
|
+
"1.0",
|
|
150
|
+
frameworkVersion:
|
|
151
|
+
source.defaults?.frameworkVersion ||
|
|
152
|
+
process.env.APP_FRAMEWORK_VERSION ||
|
|
153
|
+
"18.3.1",
|
|
154
|
+
cssIsolation: normalizeCssIsolation(
|
|
155
|
+
process.env.APP_PAGE_CSS_ISOLATION || source.defaults?.cssIsolation,
|
|
156
|
+
),
|
|
157
|
+
formMenuParentId:
|
|
158
|
+
source.defaults?.formMenuParentId ||
|
|
159
|
+
process.env.APP_FORM_MENU_PARENT_ID ||
|
|
160
|
+
"",
|
|
161
|
+
formMenuIcon:
|
|
162
|
+
source.defaults?.formMenuIcon || process.env.APP_FORM_MENU_ICON || "",
|
|
163
|
+
pageMenuParentId:
|
|
164
|
+
source.defaults?.pageMenuParentId ||
|
|
165
|
+
process.env.APP_PAGE_MENU_PARENT_ID ||
|
|
166
|
+
"",
|
|
167
|
+
pageMenuIcon:
|
|
168
|
+
source.defaults?.pageMenuIcon || process.env.APP_PAGE_MENU_ICON || "",
|
|
169
|
+
formBuilderVersion:
|
|
170
|
+
source.defaults?.formBuilderVersion ||
|
|
171
|
+
process.env.APP_FORM_BUILDER_VERSION ||
|
|
172
|
+
"2.0",
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
...normalized,
|
|
178
|
+
menu: {
|
|
179
|
+
parentId: normalized.defaults.formMenuParentId,
|
|
180
|
+
icon: normalized.defaults.formMenuIcon,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 获取后端 API 基础 URL
|
|
187
|
+
* @param {object} config - 应用配置对象
|
|
188
|
+
* @returns {string}
|
|
189
|
+
*/
|
|
190
|
+
export function getApiBaseUrl(config) {
|
|
191
|
+
return `${normalizeBaseUrl(config.platformUrl)}${config.servicePrefix}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 获取 OSS 公网访问基础 URL
|
|
196
|
+
* @param {object} config - 应用配置对象
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
export function getPublicBaseUrl(config) {
|
|
200
|
+
return `https://${config.oss.bucket}.${config.oss.region}.aliyuncs.com/${config.oss.pathPrefix}/${config.version}/${config.buildId}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 获取表单 bundle 文件的公网 URL
|
|
205
|
+
* @param {object} config - 应用配置对象
|
|
206
|
+
* @param {string} formName - 表单名
|
|
207
|
+
* @param {string} fileName - 文件名
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
export function getFormBundleUrl(config, formName, fileName) {
|
|
211
|
+
return `${getPublicBaseUrl(config)}/forms/${formName}/${fileName}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 获取表单共享 runtime 文件的公网 URL
|
|
216
|
+
* @param {object} config - 应用配置对象
|
|
217
|
+
* @param {string} fileName - 文件名
|
|
218
|
+
* @returns {string}
|
|
219
|
+
*/
|
|
220
|
+
export function getFormRuntimeUrl(config, fileName) {
|
|
221
|
+
return `${getPublicBaseUrl(config)}/form-runtime/${fileName}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 获取代码页资源基础 URL
|
|
226
|
+
* @param {object} config - 应用配置对象
|
|
227
|
+
* @param {string} pageCode - 页面编码
|
|
228
|
+
* @returns {string}
|
|
229
|
+
*/
|
|
230
|
+
export function getPageAssetBaseUrl(config, pageCode) {
|
|
231
|
+
return `${getPublicBaseUrl(config)}/pages/${pageCode}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 获取代码页指定资源的公网 URL
|
|
236
|
+
* @param {object} config - 应用配置对象
|
|
237
|
+
* @param {string} pageCode - 页面编码
|
|
238
|
+
* @param {string} fileName - 文件名
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
export function getPageAssetUrl(config, pageCode, fileName) {
|
|
242
|
+
return `${getPageAssetBaseUrl(config, pageCode)}/${fileName}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 获取代码页共享 runtime 文件的公网 URL
|
|
247
|
+
* @param {object} config - 应用配置对象
|
|
248
|
+
* @param {string} fileName - 文件名
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
export function getPageRuntimeUrl(config, fileName) {
|
|
252
|
+
return `${getPublicBaseUrl(config)}/page-runtime/${fileName}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function getBundleOSSUrl(config, formName, fileName) {
|
|
256
|
+
return getFormBundleUrl(config, formName, fileName);
|
|
257
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getApiBaseUrl,
|
|
5
|
+
resolveOpenXiangdaEndpointConfig,
|
|
6
|
+
} from "./load-config.mjs";
|
|
7
|
+
|
|
8
|
+
describe("OpenXiangda endpoint config", () => {
|
|
9
|
+
it("treats /service base as the complete API base", () => {
|
|
10
|
+
const config = resolveOpenXiangdaEndpointConfig(
|
|
11
|
+
"https://yida.wisejob.cn/service",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(config).toEqual({
|
|
15
|
+
platformUrl: "https://yida.wisejob.cn/service",
|
|
16
|
+
servicePrefix: "",
|
|
17
|
+
});
|
|
18
|
+
expect(getApiBaseUrl(config)).toBe("https://yida.wisejob.cn/service");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("maps root, platform, and view urls to the service API base", () => {
|
|
22
|
+
const inputs = [
|
|
23
|
+
"https://yida.wisejob.cn/",
|
|
24
|
+
"https://yida.wisejob.cn/platform",
|
|
25
|
+
"https://yida.wisejob.cn/platform/apps",
|
|
26
|
+
"https://yida.wisejob.cn/view/app",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const input of inputs) {
|
|
30
|
+
const config = resolveOpenXiangdaEndpointConfig(input);
|
|
31
|
+
expect(getApiBaseUrl(config)).toBe("https://yida.wisejob.cn/service");
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("preserves custom gateway paths as complete API bases", () => {
|
|
36
|
+
const config = resolveOpenXiangdaEndpointConfig(
|
|
37
|
+
"https://gateway.example.com/private-api",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(getApiBaseUrl(config)).toBe(
|
|
41
|
+
"https://gateway.example.com/private-api",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mime-types.mjs - MIME 类型映射
|
|
3
|
+
*
|
|
4
|
+
* 提供文件扩展名到 MIME 类型的映射,支持 JS/CSS/JSON/HTML、
|
|
5
|
+
* 图片、字体和其他常见静态资源类型。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
/** @type {Record<string, string>} 扩展名 → MIME 类型映射表 */
|
|
11
|
+
const MIME_MAP = {
|
|
12
|
+
// 脚本 & 样式
|
|
13
|
+
".js": "application/javascript",
|
|
14
|
+
".mjs": "application/javascript",
|
|
15
|
+
".css": "text/css",
|
|
16
|
+
".json": "application/json",
|
|
17
|
+
".html": "text/html",
|
|
18
|
+
// 图片
|
|
19
|
+
".png": "image/png",
|
|
20
|
+
".jpg": "image/jpeg",
|
|
21
|
+
".jpeg": "image/jpeg",
|
|
22
|
+
".gif": "image/gif",
|
|
23
|
+
".svg": "image/svg+xml",
|
|
24
|
+
".webp": "image/webp",
|
|
25
|
+
".ico": "image/x-icon",
|
|
26
|
+
".avif": "image/avif",
|
|
27
|
+
// 字体
|
|
28
|
+
".woff": "font/woff",
|
|
29
|
+
".woff2": "font/woff2",
|
|
30
|
+
".ttf": "font/ttf",
|
|
31
|
+
".eot": "application/vnd.ms-fontobject",
|
|
32
|
+
// 其他
|
|
33
|
+
".map": "application/json",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 根据文件路径获取对应的 MIME 类型
|
|
38
|
+
* @param {string} file - 文件路径
|
|
39
|
+
* @returns {string} MIME 类型
|
|
40
|
+
*/
|
|
41
|
+
export function getContentType(file) {
|
|
42
|
+
const ext = path.extname(file).toLowerCase();
|
|
43
|
+
return MIME_MAP[ext] || "application/octet-stream";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 判断文件名是否包含 content hash(如 logo-abc123.png)
|
|
48
|
+
* 匹配模式:name-[8+位hex hash].ext
|
|
49
|
+
* @param {string} file - 文件名或路径
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
export function hasContentHash(file) {
|
|
53
|
+
const basename = path.basename(file);
|
|
54
|
+
return /\-[a-f0-9]{8,}\.[^.]+$/.test(basename);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 根据文件特征返回合适的 Cache-Control 头
|
|
59
|
+
* - 带 content hash 的资源 → 长期缓存 + immutable
|
|
60
|
+
* - 其他文件 → 长期缓存(依赖 buildId 路径做版本隔离)
|
|
61
|
+
* @param {string} file - 文件路径
|
|
62
|
+
* @returns {string} Cache-Control 头值
|
|
63
|
+
*/
|
|
64
|
+
export function getCacheControl(file) {
|
|
65
|
+
if (hasContentHash(file)) {
|
|
66
|
+
return "public, max-age=31536000, immutable";
|
|
67
|
+
}
|
|
68
|
+
// 入口文件依赖 buildId 路径做版本隔离,同样可以长期缓存
|
|
69
|
+
return "public, max-age=31536000";
|
|
70
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const DEFAULT_PREFIX = ".sy-app-workspace";
|
|
2
|
+
|
|
3
|
+
function splitSelectors(selector) {
|
|
4
|
+
const selectors = [];
|
|
5
|
+
let depth = 0;
|
|
6
|
+
let quote = "";
|
|
7
|
+
let current = "";
|
|
8
|
+
for (const char of selector) {
|
|
9
|
+
if (quote) {
|
|
10
|
+
current += char;
|
|
11
|
+
if (char === quote) quote = "";
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (char === '"' || char === "'") {
|
|
15
|
+
quote = char;
|
|
16
|
+
current += char;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (char === "(" || char === "[") depth += 1;
|
|
20
|
+
if (char === ")" || char === "]") depth -= 1;
|
|
21
|
+
if (char === "," && depth === 0) {
|
|
22
|
+
selectors.push(current.trim());
|
|
23
|
+
current = "";
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
current += char;
|
|
27
|
+
}
|
|
28
|
+
if (current.trim()) selectors.push(current.trim());
|
|
29
|
+
return selectors;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shouldSkipSelector(selector, prefix) {
|
|
33
|
+
if (!selector || selector.includes(prefix)) return true;
|
|
34
|
+
if (/^(html|body|:root)(\b|:|\[|$)/.test(selector)) return true;
|
|
35
|
+
if (/^(@|from\b|to\b|\d+%)/.test(selector)) return true;
|
|
36
|
+
if (/^\.(ant|sy-ant|anticon|adm)-/.test(selector)) return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createNamespaceCssPlugin(prefix = DEFAULT_PREFIX) {
|
|
41
|
+
return {
|
|
42
|
+
postcssPlugin: "openxiangda-namespace-css",
|
|
43
|
+
Rule(rule) {
|
|
44
|
+
if (!rule.selector) return;
|
|
45
|
+
let parent = rule.parent;
|
|
46
|
+
while (parent) {
|
|
47
|
+
if (parent.type === "atrule" && /keyframes$/i.test(parent.name)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
parent = parent.parent;
|
|
51
|
+
}
|
|
52
|
+
rule.selector = splitSelectors(rule.selector)
|
|
53
|
+
.map((selector) =>
|
|
54
|
+
shouldSkipSelector(selector, prefix) ? selector : `${prefix} ${selector}`,
|
|
55
|
+
)
|
|
56
|
+
.join(", ");
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
createNamespaceCssPlugin.postcss = true;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oss-client.mjs - 阿里云 OSS 客户端工具
|
|
3
|
+
*
|
|
4
|
+
* 封装 OSS 客户端初始化、带重试的上传、以及 CORS 规则确保。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import OSS from "ali-oss";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 创建阿里云 OSS 客户端实例
|
|
11
|
+
* @param {object} ossConfig - OSS 配置
|
|
12
|
+
* @param {string} ossConfig.region - OSS Region
|
|
13
|
+
* @param {string} ossConfig.bucket - Bucket 名称
|
|
14
|
+
* @param {string} ossConfig.accessKeyId - AccessKey ID
|
|
15
|
+
* @param {string} ossConfig.accessKeySecret - AccessKey Secret
|
|
16
|
+
* @returns {import("ali-oss")} OSS 客户端实例
|
|
17
|
+
*/
|
|
18
|
+
export function createOSSClient(ossConfig) {
|
|
19
|
+
return new OSS({
|
|
20
|
+
region: ossConfig.region,
|
|
21
|
+
accessKeyId: ossConfig.accessKeyId,
|
|
22
|
+
accessKeySecret: ossConfig.accessKeySecret,
|
|
23
|
+
bucket: ossConfig.bucket,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 带重试机制的 OSS 上传
|
|
29
|
+
* @param {import("ali-oss")} client - OSS 客户端实例
|
|
30
|
+
* @param {string} ossKey - OSS 对象键
|
|
31
|
+
* @param {string} localPath - 本地文件路径
|
|
32
|
+
* @param {object} headers - HTTP 头(Cache-Control, Content-Type 等)
|
|
33
|
+
* @param {number} [retries=3] - 最大重试次数
|
|
34
|
+
* @returns {Promise<void>}
|
|
35
|
+
*/
|
|
36
|
+
export async function uploadWithRetry(client, ossKey, localPath, headers, retries = 3) {
|
|
37
|
+
for (let i = 0; i < retries; i++) {
|
|
38
|
+
try {
|
|
39
|
+
await client.put(ossKey, localPath, { headers });
|
|
40
|
+
return;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (i === retries - 1) throw err;
|
|
43
|
+
console.warn(` ⚠ 重试 ${i + 1}/${retries}: ${ossKey}`);
|
|
44
|
+
await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {*} value
|
|
51
|
+
* @returns {string[]}
|
|
52
|
+
*/
|
|
53
|
+
function toArray(value) {
|
|
54
|
+
if (Array.isArray(value)) return value;
|
|
55
|
+
if (value === undefined || value === null || value === "") return [];
|
|
56
|
+
return [value];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 检查单条 CORS 规则是否已满足指定 origins 和 methods
|
|
61
|
+
* @param {object} rule - CORS 规则对象
|
|
62
|
+
* @param {string[]} origins - 需要允许的来源
|
|
63
|
+
* @param {string[]} methods - 需要允许的方法
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
function corsRuleAllows(rule, origins, methods) {
|
|
67
|
+
const allowedOrigins = toArray(rule.allowedOrigin);
|
|
68
|
+
const allowedMethods = toArray(rule.allowedMethod).map((item) =>
|
|
69
|
+
String(item).toUpperCase(),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const originAllowed =
|
|
73
|
+
allowedOrigins.includes("*") ||
|
|
74
|
+
origins.every((origin) => allowedOrigins.includes(origin));
|
|
75
|
+
const methodAllowed = methods.every((method) =>
|
|
76
|
+
allowedMethods.includes(method),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return originAllowed && methodAllowed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 确保 OSS Bucket 的 CORS 规则满足前端资源加载需求
|
|
84
|
+
* @param {import("ali-oss")} client - OSS 客户端实例
|
|
85
|
+
* @param {object} ossConfig - OSS 配置
|
|
86
|
+
* @param {string} ossConfig.bucket - Bucket 名称
|
|
87
|
+
* @param {boolean} [ossConfig.skipCors] - 是否跳过 CORS 检查
|
|
88
|
+
* @param {string[]} [ossConfig.corsOrigins] - 允许的来源列表
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
export async function ensureBucketCors(client, ossConfig) {
|
|
92
|
+
if (ossConfig.skipCors) {
|
|
93
|
+
console.log(" CORS: 跳过(APP_OSS_SKIP_CORS=1)");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const origins = ossConfig.corsOrigins?.length
|
|
98
|
+
? ossConfig.corsOrigins
|
|
99
|
+
: ["*"];
|
|
100
|
+
const methods = ["GET", "HEAD"];
|
|
101
|
+
let rules = [];
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const result = await client.getBucketCORS(ossConfig.bucket);
|
|
105
|
+
rules = Array.isArray(result.rules) ? result.rules : [];
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error?.status !== 404 && error?.code !== "NoSuchCORSConfiguration") {
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (rules.some((rule) => corsRuleAllows(rule, origins, methods))) {
|
|
113
|
+
console.log(` CORS: 已允许 ${origins.join(", ")}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await client.putBucketCORS(ossConfig.bucket, [
|
|
118
|
+
...rules,
|
|
119
|
+
{
|
|
120
|
+
allowedOrigin: origins,
|
|
121
|
+
allowedMethod: methods,
|
|
122
|
+
allowedHeader: "*",
|
|
123
|
+
exposeHeader: ["ETag", "Content-Length", "Content-Type"],
|
|
124
|
+
maxAgeSeconds: "3600",
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
console.log(` CORS: 已配置 ${origins.join(", ")}`);
|
|
128
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { rootDir as workspaceRoot } from "./load-config.mjs";
|
|
5
|
+
|
|
6
|
+
export const rootDir = workspaceRoot;
|
|
7
|
+
export const srcRoot = path.join(rootDir, "src");
|
|
8
|
+
export const formsRoot = path.join(srcRoot, "forms");
|
|
9
|
+
export const pagesRoot = path.join(srcRoot, "pages");
|
|
10
|
+
export const distRoot = path.join(rootDir, "dist");
|
|
11
|
+
|
|
12
|
+
async function fileExists(targetPath) {
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(targetPath);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function loadTypeScriptModule(filePath) {
|
|
22
|
+
const loaded = await import(pathToFileURL(filePath).href);
|
|
23
|
+
return loaded.default || loaded;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function discoverPages(filterName = "") {
|
|
27
|
+
if (!(await fileExists(pagesRoot))) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const entries = await fs.readdir(pagesRoot, { withFileTypes: true });
|
|
32
|
+
const pages = [];
|
|
33
|
+
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (!entry.isDirectory()) continue;
|
|
36
|
+
if (filterName && entry.name !== filterName) continue;
|
|
37
|
+
|
|
38
|
+
const dirPath = path.join(pagesRoot, entry.name);
|
|
39
|
+
const entryPath = path.join(dirPath, "index.tsx");
|
|
40
|
+
const appPath = path.join(dirPath, "App.tsx");
|
|
41
|
+
const configPath = path.join(dirPath, "page.config.ts");
|
|
42
|
+
const required = [entryPath, appPath, configPath];
|
|
43
|
+
const exists = await Promise.all(required.map(fileExists));
|
|
44
|
+
if (exists.every((item) => !item)) continue;
|
|
45
|
+
for (let index = 0; index < required.length; index += 1) {
|
|
46
|
+
if (!exists[index]) {
|
|
47
|
+
throw new Error(`页面 ${entry.name} 缺少文件: ${path.basename(required[index])}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const config = await loadTypeScriptModule(configPath);
|
|
52
|
+
if (config.publish === false) continue;
|
|
53
|
+
pages.push({
|
|
54
|
+
dirName: entry.name,
|
|
55
|
+
dirPath,
|
|
56
|
+
entryPath,
|
|
57
|
+
appPath,
|
|
58
|
+
configPath,
|
|
59
|
+
config,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return pages.sort((left, right) =>
|
|
64
|
+
String(left.config.name || left.dirName).localeCompare(
|
|
65
|
+
String(right.config.name || right.dirName),
|
|
66
|
+
"zh-Hans-CN",
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function readPackageJson() {
|
|
72
|
+
return JSON.parse(await fs.readFile(path.join(rootDir, "package.json"), "utf8"));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function getReactVersion() {
|
|
76
|
+
const packageJson = await readPackageJson();
|
|
77
|
+
return String(packageJson.dependencies?.react || "")
|
|
78
|
+
.replace(/^[~^]/, "")
|
|
79
|
+
.trim();
|
|
80
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* progress.mjs - 上传进度报告工具
|
|
3
|
+
*
|
|
4
|
+
* 提供文件上传进度跟踪,包括已完成数量、文件总大小、耗时统计。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 创建进度跟踪器
|
|
11
|
+
* @param {number} total - 总文件数
|
|
12
|
+
* @returns {{ tick: (file: string, localPath: string) => void, summary: () => void }}
|
|
13
|
+
*/
|
|
14
|
+
export function createProgressTracker(total) {
|
|
15
|
+
let completed = 0;
|
|
16
|
+
let totalBytes = 0;
|
|
17
|
+
const startTime = Date.now();
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
/**
|
|
21
|
+
* 标记一个文件上传完成
|
|
22
|
+
* @param {string} ossKey - OSS 对象键(用于日志)
|
|
23
|
+
* @param {string} localPath - 本地文件路径(用于计算大小)
|
|
24
|
+
*/
|
|
25
|
+
tick(ossKey, localPath) {
|
|
26
|
+
completed += 1;
|
|
27
|
+
try {
|
|
28
|
+
const stat = fs.statSync(localPath);
|
|
29
|
+
totalBytes += stat.size;
|
|
30
|
+
} catch {
|
|
31
|
+
// 忽略 stat 失败
|
|
32
|
+
}
|
|
33
|
+
const pct = Math.round((completed / total) * 100);
|
|
34
|
+
console.log(` ✓ [${completed}/${total} ${pct}%] ${ossKey}`);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 标记一个文件上传失败
|
|
39
|
+
* @param {string} ossKey - OSS 对象键
|
|
40
|
+
* @param {Error} error - 错误对象
|
|
41
|
+
*/
|
|
42
|
+
fail(ossKey, error) {
|
|
43
|
+
completed += 1;
|
|
44
|
+
const pct = Math.round((completed / total) * 100);
|
|
45
|
+
console.error(` ✗ [${completed}/${total} ${pct}%] ${ossKey}: ${error.message}`);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 打印上传汇总信息
|
|
50
|
+
*/
|
|
51
|
+
summary() {
|
|
52
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
53
|
+
const sizeMB = (totalBytes / (1024 * 1024)).toFixed(2);
|
|
54
|
+
console.log(`\n📊 上传统计: ${total} 个文件, ${sizeMB} MB, 耗时 ${elapsed}s`);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|