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,244 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { ensureSchemaFormUuid } from "./form-api.mjs";
|
|
8
|
+
|
|
9
|
+
const config = {
|
|
10
|
+
platformUrl: "https://platform.example.com",
|
|
11
|
+
servicePrefix: "/service",
|
|
12
|
+
appType: "APP_TEST",
|
|
13
|
+
appKey: "app-key",
|
|
14
|
+
appSecret: "app-secret",
|
|
15
|
+
userId: "user-1",
|
|
16
|
+
defaults: {
|
|
17
|
+
formBuilderVersion: "2.0",
|
|
18
|
+
},
|
|
19
|
+
menu: {
|
|
20
|
+
parentId: "",
|
|
21
|
+
icon: "",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let tempDirs: string[] = [];
|
|
26
|
+
|
|
27
|
+
function createSchemaFile() {
|
|
28
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "form-api-"));
|
|
29
|
+
tempDirs.push(dir);
|
|
30
|
+
const schemaPath = path.join(dir, "schema.ts");
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
schemaPath,
|
|
33
|
+
`export default { formMeta: { formUuid: "FORM_1", appType: "APP_TEST", title: "客户表单", formType: "receipt" } };\n`,
|
|
34
|
+
"utf-8",
|
|
35
|
+
);
|
|
36
|
+
return schemaPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
vi.unstubAllGlobals();
|
|
41
|
+
for (const dir of tempDirs) {
|
|
42
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
tempDirs = [];
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("form api provisioning helpers", () => {
|
|
48
|
+
it("does not create a menu for an existing schema formUuid unless requested", async () => {
|
|
49
|
+
const schemaPath = createSchemaFile();
|
|
50
|
+
const fetchMock = vi.fn(async (url: string) => {
|
|
51
|
+
if (url.endsWith("/forms/getFormList")) {
|
|
52
|
+
return Response.json({
|
|
53
|
+
code: 200,
|
|
54
|
+
data: [{ formUuid: "FORM_1", appType: "APP_TEST" }],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (url.endsWith("/forms/menu/create")) {
|
|
58
|
+
return Response.json({ code: 200, data: { id: "MENU_1" } });
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`unexpected request: ${url}`);
|
|
61
|
+
});
|
|
62
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
63
|
+
|
|
64
|
+
const result = await ensureSchemaFormUuid({
|
|
65
|
+
config,
|
|
66
|
+
schemaPath,
|
|
67
|
+
formName: "customer",
|
|
68
|
+
accessToken: "token",
|
|
69
|
+
ensureMenu: false,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result.formUuid).toBe("FORM_1");
|
|
73
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(
|
|
75
|
+
fetchMock.mock.calls.map(([url]) => String(url)).join("\n"),
|
|
76
|
+
).not.toContain("/forms/menu/create");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("does not create a menu for an existing schema formUuid by default", async () => {
|
|
80
|
+
const schemaPath = createSchemaFile();
|
|
81
|
+
const fetchMock = vi.fn(async (url: string) => {
|
|
82
|
+
if (url.endsWith("/forms/getFormList")) {
|
|
83
|
+
return Response.json({
|
|
84
|
+
code: 200,
|
|
85
|
+
data: [{ formUuid: "FORM_1", appType: "APP_TEST" }],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (url.endsWith("/forms/menu/create")) {
|
|
89
|
+
return Response.json({ code: 200, data: { id: "MENU_1" } });
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`unexpected request: ${url}`);
|
|
92
|
+
});
|
|
93
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
94
|
+
|
|
95
|
+
await ensureSchemaFormUuid({
|
|
96
|
+
config,
|
|
97
|
+
schemaPath,
|
|
98
|
+
formName: "customer",
|
|
99
|
+
accessToken: "token",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
103
|
+
expect(
|
|
104
|
+
fetchMock.mock.calls.map(([url]) => String(url)).join("\n"),
|
|
105
|
+
).not.toContain("/forms/menu/create");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("creates a menu for an existing schema formUuid when explicitly requested", async () => {
|
|
109
|
+
const schemaPath = createSchemaFile();
|
|
110
|
+
const fetchMock = vi.fn(async (url: string) => {
|
|
111
|
+
if (url.endsWith("/forms/getFormList")) {
|
|
112
|
+
return Response.json({
|
|
113
|
+
code: 200,
|
|
114
|
+
data: [{ formUuid: "FORM_1", appType: "APP_TEST" }],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (url.endsWith("/forms/getMenusByAppType")) {
|
|
118
|
+
return Response.json({ code: 200, data: [] });
|
|
119
|
+
}
|
|
120
|
+
if (url.endsWith("/forms/menu/create")) {
|
|
121
|
+
return Response.json({ code: 200, data: { id: "MENU_1" } });
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`unexpected request: ${url}`);
|
|
124
|
+
});
|
|
125
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
126
|
+
|
|
127
|
+
await ensureSchemaFormUuid({
|
|
128
|
+
config,
|
|
129
|
+
schemaPath,
|
|
130
|
+
formName: "customer",
|
|
131
|
+
accessToken: "token",
|
|
132
|
+
ensureMenu: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(fetchMock.mock.calls.map(([url]) => String(url))).toContain(
|
|
136
|
+
"https://platform.example.com/service/dingtalk-api/v1.0/forms/menu/create",
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("creates a menu when a schema formUuid form is created this run", async () => {
|
|
141
|
+
const schemaPath = createSchemaFile();
|
|
142
|
+
const fetchMock = vi.fn(async (url: string) => {
|
|
143
|
+
if (url.endsWith("/forms/getFormList")) {
|
|
144
|
+
return Response.json({ code: 200, data: [] });
|
|
145
|
+
}
|
|
146
|
+
if (url.endsWith("/forms/createForm")) {
|
|
147
|
+
return Response.json({ code: 200, data: { formUuid: "FORM_1" } });
|
|
148
|
+
}
|
|
149
|
+
if (url.endsWith("/forms/getMenusByAppType")) {
|
|
150
|
+
return Response.json({ code: 200, data: [] });
|
|
151
|
+
}
|
|
152
|
+
if (url.endsWith("/forms/menu/create")) {
|
|
153
|
+
return Response.json({ code: 200, data: { id: "MENU_1" } });
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`unexpected request: ${url}`);
|
|
156
|
+
});
|
|
157
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
158
|
+
|
|
159
|
+
const result = await ensureSchemaFormUuid({
|
|
160
|
+
config,
|
|
161
|
+
schemaPath,
|
|
162
|
+
formName: "customer",
|
|
163
|
+
accessToken: "token",
|
|
164
|
+
ensureMenu: false,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(result.created).toBe(true);
|
|
168
|
+
expect(fetchMock.mock.calls.map(([url]) => String(url))).toContain(
|
|
169
|
+
"https://platform.example.com/service/dingtalk-api/v1.0/forms/menu/create",
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("uses the bound OpenXiangda appType and recreates stale imported form ids", async () => {
|
|
174
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "form-api-"));
|
|
175
|
+
tempDirs.push(dir);
|
|
176
|
+
const schemaPath = path.join(dir, "schema.ts");
|
|
177
|
+
const metaPath = path.join(dir, "meta.ts");
|
|
178
|
+
fs.writeFileSync(
|
|
179
|
+
metaPath,
|
|
180
|
+
[
|
|
181
|
+
'export const CUSTOMER_INFO_FORM_UUID = "FORM_OLD";',
|
|
182
|
+
'export const CUSTOMER_INFO_APP_TYPE = "APP_OLD";',
|
|
183
|
+
'export const CUSTOMER_INFO_TITLE = "客户表单";',
|
|
184
|
+
"",
|
|
185
|
+
].join("\n"),
|
|
186
|
+
"utf-8",
|
|
187
|
+
);
|
|
188
|
+
fs.writeFileSync(
|
|
189
|
+
schemaPath,
|
|
190
|
+
[
|
|
191
|
+
"import {",
|
|
192
|
+
" CUSTOMER_INFO_APP_TYPE,",
|
|
193
|
+
" CUSTOMER_INFO_FORM_UUID,",
|
|
194
|
+
" CUSTOMER_INFO_TITLE,",
|
|
195
|
+
'} from "./meta";',
|
|
196
|
+
"export default {",
|
|
197
|
+
" formMeta: {",
|
|
198
|
+
" formUuid: CUSTOMER_INFO_FORM_UUID,",
|
|
199
|
+
" appType: CUSTOMER_INFO_APP_TYPE,",
|
|
200
|
+
" title: CUSTOMER_INFO_TITLE,",
|
|
201
|
+
' formType: "receipt",',
|
|
202
|
+
" },",
|
|
203
|
+
"};",
|
|
204
|
+
"",
|
|
205
|
+
].join("\n"),
|
|
206
|
+
"utf-8",
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
|
|
210
|
+
expect(String(url)).toBe(
|
|
211
|
+
"https://platform.example.com/service/openxiangda-api/v1/apps/APP_TEST/forms",
|
|
212
|
+
);
|
|
213
|
+
const body = JSON.parse(String(init?.body || "{}"));
|
|
214
|
+
expect(body.formUuid).toBeUndefined();
|
|
215
|
+
expect(body.name).toBe("客户表单");
|
|
216
|
+
return Response.json({
|
|
217
|
+
code: 200,
|
|
218
|
+
data: { form: { formUuid: "FORM_NEW" } },
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
222
|
+
|
|
223
|
+
const result = await ensureSchemaFormUuid({
|
|
224
|
+
config: {
|
|
225
|
+
...config,
|
|
226
|
+
openXiangdaAccessToken: "token",
|
|
227
|
+
},
|
|
228
|
+
schemaPath,
|
|
229
|
+
formName: "customer",
|
|
230
|
+
accessToken: "token",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(result.created).toBe(true);
|
|
234
|
+
expect(result.formUuid).toBe("FORM_NEW");
|
|
235
|
+
expect(result.appType).toBe("APP_TEST");
|
|
236
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
237
|
+
expect(fs.readFileSync(metaPath, "utf-8")).toContain(
|
|
238
|
+
'CUSTOMER_INFO_FORM_UUID = "FORM_NEW"',
|
|
239
|
+
);
|
|
240
|
+
expect(fs.readFileSync(metaPath, "utf-8")).toContain(
|
|
241
|
+
'CUSTOMER_INFO_APP_TYPE = "APP_TEST"',
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getFormBundleUrl } from "./load-config.mjs";
|
|
5
|
+
|
|
6
|
+
export const runtimeCssMarker = "/* sy-lowcode shared form runtime styles */";
|
|
7
|
+
export const formCssMarker = "/* sy-lowcode form bundle styles */";
|
|
8
|
+
|
|
9
|
+
export function isNonEmptyFile(filePath) {
|
|
10
|
+
return Boolean(
|
|
11
|
+
filePath && fs.existsSync(filePath) && fs.statSync(filePath).size > 0,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveRuntimeCssPath(runtimeDistDir, manifest) {
|
|
16
|
+
const cssFile = manifest?.files?.css;
|
|
17
|
+
if (!cssFile) return "";
|
|
18
|
+
return path.resolve(runtimeDistDir, cssFile);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function mergeSharedRuntimeCssIntoFormCss({
|
|
22
|
+
runtimeCssPath,
|
|
23
|
+
formCssPath,
|
|
24
|
+
}) {
|
|
25
|
+
if (!isNonEmptyFile(runtimeCssPath)) {
|
|
26
|
+
return { merged: false, reason: "missing-runtime-css" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const runtimeCss = fs.readFileSync(runtimeCssPath, "utf-8").trimEnd();
|
|
30
|
+
const formCss = fs.existsSync(formCssPath)
|
|
31
|
+
? fs.readFileSync(formCssPath, "utf-8")
|
|
32
|
+
: "";
|
|
33
|
+
|
|
34
|
+
if (formCss.includes(runtimeCssMarker)) {
|
|
35
|
+
return { merged: false, reason: "already-merged" };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const nextCss = formCss.trim()
|
|
39
|
+
? `${runtimeCssMarker}\n${runtimeCss}\n\n${formCssMarker}\n${formCss.trimStart()}`
|
|
40
|
+
: `${runtimeCssMarker}\n${runtimeCss}\n`;
|
|
41
|
+
|
|
42
|
+
fs.mkdirSync(path.dirname(formCssPath), { recursive: true });
|
|
43
|
+
fs.writeFileSync(formCssPath, nextCss, "utf-8");
|
|
44
|
+
return { merged: true, reason: "merged" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getRegisteredFormCssUrl(config, formName, options = {}) {
|
|
48
|
+
const formCssUrl = getFormBundleUrl(config, formName, "style.css");
|
|
49
|
+
if (
|
|
50
|
+
options.runtime?.cssUrl &&
|
|
51
|
+
options.formCssPath &&
|
|
52
|
+
!isNonEmptyFile(options.formCssPath)
|
|
53
|
+
) {
|
|
54
|
+
return options.runtime.cssUrl;
|
|
55
|
+
}
|
|
56
|
+
return formCssUrl;
|
|
57
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
formCssMarker,
|
|
9
|
+
getRegisteredFormCssUrl,
|
|
10
|
+
mergeSharedRuntimeCssIntoFormCss,
|
|
11
|
+
resolveRuntimeCssPath,
|
|
12
|
+
runtimeCssMarker,
|
|
13
|
+
} from "./form-runtime-assets.mjs";
|
|
14
|
+
|
|
15
|
+
const config = {
|
|
16
|
+
version: "1.2.3",
|
|
17
|
+
buildId: "BUILD_001",
|
|
18
|
+
oss: {
|
|
19
|
+
bucket: "bucket",
|
|
20
|
+
region: "oss-cn-hangzhou",
|
|
21
|
+
pathPrefix: "lowcode/app-workspace/dev",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let tempDirs: string[] = [];
|
|
26
|
+
|
|
27
|
+
function createTempDir() {
|
|
28
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "form-runtime-assets-"));
|
|
29
|
+
tempDirs.push(dir);
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
for (const dir of tempDirs) {
|
|
35
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
tempDirs = [];
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("form runtime asset helpers", () => {
|
|
41
|
+
it("resolves the runtime css path from the manifest", () => {
|
|
42
|
+
expect(
|
|
43
|
+
resolveRuntimeCssPath("/workspace/dist/form-runtime", {
|
|
44
|
+
files: { css: "style.css" },
|
|
45
|
+
}),
|
|
46
|
+
).toBe("/workspace/dist/form-runtime/style.css");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("copies shared runtime css into an empty form css file", () => {
|
|
50
|
+
const dir = createTempDir();
|
|
51
|
+
const runtimeCssPath = path.join(dir, "runtime.css");
|
|
52
|
+
const formCssPath = path.join(dir, "forms", "style.css");
|
|
53
|
+
fs.writeFileSync(
|
|
54
|
+
runtimeCssPath,
|
|
55
|
+
".adm-button{display:inline-flex}",
|
|
56
|
+
"utf-8",
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const result = mergeSharedRuntimeCssIntoFormCss({
|
|
60
|
+
runtimeCssPath,
|
|
61
|
+
formCssPath,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result).toEqual({ merged: true, reason: "merged" });
|
|
65
|
+
expect(fs.readFileSync(formCssPath, "utf-8")).toContain(runtimeCssMarker);
|
|
66
|
+
expect(fs.readFileSync(formCssPath, "utf-8")).toContain(".adm-button");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("keeps form css after the shared runtime css", () => {
|
|
70
|
+
const dir = createTempDir();
|
|
71
|
+
const runtimeCssPath = path.join(dir, "runtime.css");
|
|
72
|
+
const formCssPath = path.join(dir, "style.css");
|
|
73
|
+
fs.writeFileSync(runtimeCssPath, ".runtime{color:red}", "utf-8");
|
|
74
|
+
fs.writeFileSync(formCssPath, ".form{color:blue}", "utf-8");
|
|
75
|
+
|
|
76
|
+
mergeSharedRuntimeCssIntoFormCss({ runtimeCssPath, formCssPath });
|
|
77
|
+
|
|
78
|
+
const css = fs.readFileSync(formCssPath, "utf-8");
|
|
79
|
+
expect(css.indexOf(runtimeCssMarker)).toBeLessThan(
|
|
80
|
+
css.indexOf(formCssMarker),
|
|
81
|
+
);
|
|
82
|
+
expect(css).toContain(".runtime");
|
|
83
|
+
expect(css).toContain(".form");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("does not duplicate runtime css when the file is already merged", () => {
|
|
87
|
+
const dir = createTempDir();
|
|
88
|
+
const runtimeCssPath = path.join(dir, "runtime.css");
|
|
89
|
+
const formCssPath = path.join(dir, "style.css");
|
|
90
|
+
fs.writeFileSync(runtimeCssPath, ".runtime{color:red}", "utf-8");
|
|
91
|
+
fs.writeFileSync(
|
|
92
|
+
formCssPath,
|
|
93
|
+
`${runtimeCssMarker}\n.runtime{color:red}\n`,
|
|
94
|
+
"utf-8",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const result = mergeSharedRuntimeCssIntoFormCss({
|
|
98
|
+
runtimeCssPath,
|
|
99
|
+
formCssPath,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result).toEqual({ merged: false, reason: "already-merged" });
|
|
103
|
+
expect(
|
|
104
|
+
fs.readFileSync(formCssPath, "utf-8").match(/runtime/g),
|
|
105
|
+
).toHaveLength(2);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("falls back to runtime css for register when form css is empty", () => {
|
|
109
|
+
const dir = createTempDir();
|
|
110
|
+
const formCssPath = path.join(dir, "style.css");
|
|
111
|
+
fs.writeFileSync(formCssPath, "", "utf-8");
|
|
112
|
+
|
|
113
|
+
expect(
|
|
114
|
+
getRegisteredFormCssUrl(config, "customer-info", {
|
|
115
|
+
formCssPath,
|
|
116
|
+
runtime: { cssUrl: "https://cdn.example.com/form-runtime/style.css" },
|
|
117
|
+
}),
|
|
118
|
+
).toBe("https://cdn.example.com/form-runtime/style.css");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("keeps the form css url for register when form css has content", () => {
|
|
122
|
+
const dir = createTempDir();
|
|
123
|
+
const formCssPath = path.join(dir, "style.css");
|
|
124
|
+
fs.writeFileSync(formCssPath, ".form{color:blue}", "utf-8");
|
|
125
|
+
|
|
126
|
+
expect(
|
|
127
|
+
getRegisteredFormCssUrl(config, "customer-info", {
|
|
128
|
+
formCssPath,
|
|
129
|
+
runtime: { cssUrl: "https://cdn.example.com/form-runtime/style.css" },
|
|
130
|
+
}),
|
|
131
|
+
).toBe(
|
|
132
|
+
"https://bucket.oss-cn-hangzhou.aliyuncs.com/lowcode/app-workspace/dev/1.2.3/BUILD_001/forms/customer-info/style.css",
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { rootDir } from "./load-config.mjs";
|
|
5
|
+
|
|
6
|
+
const CACHE_VERSION = 1;
|
|
7
|
+
export const CACHE_FILE = path.join(rootDir, ".openxiangda", "build-cache.json");
|
|
8
|
+
|
|
9
|
+
function toPosix(filePath) {
|
|
10
|
+
return filePath.split(path.sep).join("/");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hashContent(content) {
|
|
14
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readFileHash(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return hashContent(fs.readFileSync(filePath));
|
|
20
|
+
} catch {
|
|
21
|
+
return "missing";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function walkFiles(dirPath, files = []) {
|
|
26
|
+
if (!fs.existsSync(dirPath)) return files;
|
|
27
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
28
|
+
const nextPath = path.join(dirPath, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
if (["node_modules", "dist", ".openxiangda", ".git", ".tmp"].includes(entry.name)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
walkFiles(nextPath, files);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (entry.isFile()) files.push(nextPath);
|
|
37
|
+
}
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function computeDirectoryHash(dirPath) {
|
|
42
|
+
const files = walkFiles(dirPath).sort((left, right) =>
|
|
43
|
+
toPosix(path.relative(rootDir, left)).localeCompare(toPosix(path.relative(rootDir, right))),
|
|
44
|
+
);
|
|
45
|
+
const hash = crypto.createHash("sha256");
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const rel = toPosix(path.relative(rootDir, file));
|
|
48
|
+
hash.update(rel);
|
|
49
|
+
hash.update("\0");
|
|
50
|
+
hash.update(readFileHash(file));
|
|
51
|
+
hash.update("\0");
|
|
52
|
+
}
|
|
53
|
+
return hash.digest("hex");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function computeLockfileHash() {
|
|
57
|
+
const hash = crypto.createHash("sha256");
|
|
58
|
+
for (const name of ["pnpm-lock.yaml", "package-lock.json", "yarn.lock"]) {
|
|
59
|
+
const filePath = path.join(rootDir, name);
|
|
60
|
+
hash.update(name);
|
|
61
|
+
hash.update("\0");
|
|
62
|
+
hash.update(readFileHash(filePath));
|
|
63
|
+
hash.update("\0");
|
|
64
|
+
}
|
|
65
|
+
return hash.digest("hex");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function computeSharedHash() {
|
|
69
|
+
const sharedDir = path.join(rootDir, "src", "shared");
|
|
70
|
+
return fs.existsSync(sharedDir) ? computeDirectoryHash(sharedDir) : "no-shared";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function computeConfigHash() {
|
|
74
|
+
const hash = crypto.createHash("sha256");
|
|
75
|
+
for (const name of [
|
|
76
|
+
"package.json",
|
|
77
|
+
"app-workspace.config.ts",
|
|
78
|
+
"tailwind.config.cjs",
|
|
79
|
+
"postcss.config.cjs",
|
|
80
|
+
"vite.config.ts",
|
|
81
|
+
"tsconfig.json",
|
|
82
|
+
"src/index.css",
|
|
83
|
+
]) {
|
|
84
|
+
hash.update(name);
|
|
85
|
+
hash.update("\0");
|
|
86
|
+
hash.update(readFileHash(path.join(rootDir, name)));
|
|
87
|
+
hash.update("\0");
|
|
88
|
+
}
|
|
89
|
+
return hash.digest("hex");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function loadBuildCache() {
|
|
93
|
+
try {
|
|
94
|
+
const cache = JSON.parse(fs.readFileSync(CACHE_FILE, "utf-8"));
|
|
95
|
+
return cache.version === CACHE_VERSION ? cache : null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function cleanBuildCache() {
|
|
102
|
+
fs.rmSync(CACHE_FILE, { force: true });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function normalizeOnly(value) {
|
|
106
|
+
if (!value) return [];
|
|
107
|
+
const raw = Array.isArray(value) ? value.join(",") : String(value);
|
|
108
|
+
return raw
|
|
109
|
+
.split(",")
|
|
110
|
+
.map((item) => item.trim().replace(/^src\//, ""))
|
|
111
|
+
.filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function planIncrementalBuild(modules, options = {}) {
|
|
115
|
+
const only = normalizeOnly(options.only);
|
|
116
|
+
const selected = only.length
|
|
117
|
+
? modules.filter((item) => only.includes(item.key))
|
|
118
|
+
: modules;
|
|
119
|
+
const moduleHashes = Object.fromEntries(
|
|
120
|
+
modules.map((item) => [item.key, computeDirectoryHash(item.dirPath)]),
|
|
121
|
+
);
|
|
122
|
+
const snapshot = {
|
|
123
|
+
lockfileHash: computeLockfileHash(),
|
|
124
|
+
sharedHash: computeSharedHash(),
|
|
125
|
+
configHash: computeConfigHash(),
|
|
126
|
+
moduleHashes,
|
|
127
|
+
};
|
|
128
|
+
const cache = loadBuildCache();
|
|
129
|
+
const fullRebuild =
|
|
130
|
+
Boolean(options.force) ||
|
|
131
|
+
!cache ||
|
|
132
|
+
cache.lockfileHash !== snapshot.lockfileHash ||
|
|
133
|
+
cache.sharedHash !== snapshot.sharedHash ||
|
|
134
|
+
cache.configHash !== snapshot.configHash;
|
|
135
|
+
const changed = selected.filter(
|
|
136
|
+
(item) => fullRebuild || cache?.entries?.[item.key]?.contentHash !== moduleHashes[item.key],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
all: modules,
|
|
141
|
+
selected,
|
|
142
|
+
changed,
|
|
143
|
+
fullRebuild,
|
|
144
|
+
snapshot,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function discoverWorkspaceModules() {
|
|
149
|
+
const modules = [];
|
|
150
|
+
for (const [kind, requiredFile] of [
|
|
151
|
+
["forms", "schema.ts"],
|
|
152
|
+
["pages", "page.config.ts"],
|
|
153
|
+
]) {
|
|
154
|
+
const baseDir = path.join(rootDir, "src", kind);
|
|
155
|
+
if (!fs.existsSync(baseDir)) continue;
|
|
156
|
+
for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
|
|
157
|
+
if (!entry.isDirectory()) continue;
|
|
158
|
+
const dirPath = path.join(baseDir, entry.name);
|
|
159
|
+
if (!fs.existsSync(path.join(dirPath, requiredFile))) continue;
|
|
160
|
+
modules.push({
|
|
161
|
+
key: `${kind}/${entry.name}`,
|
|
162
|
+
kind,
|
|
163
|
+
name: entry.name,
|
|
164
|
+
dirPath,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return modules.sort((left, right) => left.key.localeCompare(right.key));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function commitIncrementalBuild(plan, builtModules) {
|
|
172
|
+
const previous = loadBuildCache();
|
|
173
|
+
const entries = { ...(previous?.entries || {}) };
|
|
174
|
+
const now = new Date().toISOString();
|
|
175
|
+
for (const item of builtModules) {
|
|
176
|
+
entries[item.key] = {
|
|
177
|
+
contentHash: plan.snapshot.moduleHashes[item.key],
|
|
178
|
+
lastBuildTime: now,
|
|
179
|
+
success: true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
|
|
183
|
+
fs.writeFileSync(
|
|
184
|
+
CACHE_FILE,
|
|
185
|
+
`${JSON.stringify(
|
|
186
|
+
{
|
|
187
|
+
version: CACHE_VERSION,
|
|
188
|
+
lockfileHash: plan.snapshot.lockfileHash,
|
|
189
|
+
sharedHash: plan.snapshot.sharedHash,
|
|
190
|
+
configHash: plan.snapshot.configHash,
|
|
191
|
+
entries,
|
|
192
|
+
},
|
|
193
|
+
null,
|
|
194
|
+
2,
|
|
195
|
+
)}\n`,
|
|
196
|
+
"utf-8",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function printPlan(plan, label) {
|
|
201
|
+
if (plan.changed.length === 0) {
|
|
202
|
+
console.log(`[build] ${label}: No changes detected`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const mode = plan.fullRebuild ? "full rebuild" : "incremental";
|
|
206
|
+
console.log(
|
|
207
|
+
`[build] ${label}: Building ${plan.changed.length}/${plan.all.length} module(s) (${mode})`,
|
|
208
|
+
);
|
|
209
|
+
for (const item of plan.changed) console.log(` - ${item.key}`);
|
|
210
|
+
}
|