openxiangda-cli 2.0.0-alpha.91 → 2.0.0-alpha.96
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/bin/run.js +20 -0
- package/package.json +5 -5
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/resources.spec.ts +16 -0
- package/template/apps/web/e2e/workflow.spec.ts +329 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/playwright.config.ts +1 -0
- package/template/apps/web/scripts/verify-build.mjs +3 -29
- package/template/apps/web/src/main.tsx +2 -0
- package/template/apps/web/test/contracts.test.ts +27 -0
- package/template/package.json +1 -1
package/bin/run.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execute } from '@oclif/core';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
|
|
5
6
|
const publicCommands = new Set([
|
|
@@ -58,10 +59,29 @@ if (args.includes('--mcp-stdio')) {
|
|
|
58
59
|
process.exitCode = 2;
|
|
59
60
|
} else {
|
|
60
61
|
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
62
|
+
const cliManifest = JSON.parse(
|
|
63
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
64
|
+
);
|
|
65
|
+
const distributionManifest = process.env.OPENXIANGDA_DISTRIBUTION_NAME
|
|
66
|
+
? {
|
|
67
|
+
...cliManifest,
|
|
68
|
+
name: process.env.OPENXIANGDA_DISTRIBUTION_NAME,
|
|
69
|
+
version:
|
|
70
|
+
process.env.OPENXIANGDA_DISTRIBUTION_VERSION ||
|
|
71
|
+
cliManifest.version,
|
|
72
|
+
}
|
|
73
|
+
: undefined;
|
|
61
74
|
await execute({
|
|
62
75
|
args,
|
|
63
76
|
loadOptions: {
|
|
64
77
|
root,
|
|
78
|
+
...(distributionManifest ? { pjson: distributionManifest } : {}),
|
|
79
|
+
...(process.env.OPENXIANGDA_DISTRIBUTION_NAME
|
|
80
|
+
? { name: process.env.OPENXIANGDA_DISTRIBUTION_NAME }
|
|
81
|
+
: {}),
|
|
82
|
+
...(process.env.OPENXIANGDA_DISTRIBUTION_VERSION
|
|
83
|
+
? { version: process.env.OPENXIANGDA_DISTRIBUTION_VERSION }
|
|
84
|
+
: {}),
|
|
65
85
|
userPlugins: false,
|
|
66
86
|
devPlugins: false,
|
|
67
87
|
jitPlugins: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openxiangda-cli",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.96",
|
|
4
4
|
"description": "Thin application-level CLI for OpenXiangda 2.0.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@oclif/core": "4.13.3",
|
|
24
|
-
"openxiangda-contracts": "2.0.0-alpha.
|
|
25
|
-
"openxiangda-devkit-core": "2.0.0-alpha.
|
|
26
|
-
"openxiangda-mcp": "2.0.0-alpha.
|
|
27
|
-
"openxiangda-skill-kit": "2.0.0-alpha.
|
|
24
|
+
"openxiangda-contracts": "2.0.0-alpha.46",
|
|
25
|
+
"openxiangda-devkit-core": "2.0.0-alpha.59",
|
|
26
|
+
"openxiangda-mcp": "2.0.0-alpha.59",
|
|
27
|
+
"openxiangda-skill-kit": "2.0.0-alpha.77"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsx": "4.23.12",
|
|
@@ -96,6 +96,22 @@ async function mockPlatform(
|
|
|
96
96
|
}),
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
|
+
const exportRequest = url.pathname.match(
|
|
100
|
+
/\/native\/data\/([^/]+)\/export$/
|
|
101
|
+
);
|
|
102
|
+
if (exportRequest) {
|
|
103
|
+
observed.push({ path: url.pathname, roleSessionId });
|
|
104
|
+
return route.fulfill({
|
|
105
|
+
status: 200,
|
|
106
|
+
contentType: 'text/csv; charset=utf-8',
|
|
107
|
+
headers: {
|
|
108
|
+
'content-disposition': `attachment; filename="${decodeURIComponent(
|
|
109
|
+
exportRequest[1]
|
|
110
|
+
)}.csv"`,
|
|
111
|
+
},
|
|
112
|
+
body: 'id,name\nacceptance-1,OpenXiangda\n',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
99
115
|
const query = url.pathname.match(/\/native\/data\/([^/]+)\/query$/);
|
|
100
116
|
if (query) {
|
|
101
117
|
observed.push({ path: url.pathname, roleSessionId });
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { expect, test, type Page } from '@playwright/test';
|
|
2
|
+
import { appCode } from '../../../packages/contracts/src/generated.js';
|
|
3
|
+
|
|
4
|
+
const runtimeBase = `/view/${appCode}`;
|
|
5
|
+
const taskId = '11111111-1111-4111-8111-111111111111';
|
|
6
|
+
const instanceId = '22222222-2222-4222-8222-222222222222';
|
|
7
|
+
function envelope(data: unknown, code = 200) {
|
|
8
|
+
return { code, message: code === 200 ? 'success' : 'forbidden', data };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function task(completed = false) {
|
|
12
|
+
return {
|
|
13
|
+
schemaVersion: 'openxiangda.workflow-task/v2',
|
|
14
|
+
engineVersion: '2.0',
|
|
15
|
+
id: taskId,
|
|
16
|
+
instanceId,
|
|
17
|
+
workflowCode: 'purchase-approval',
|
|
18
|
+
businessKey: 'PO-2026-0825',
|
|
19
|
+
nodeId: 'manager-review',
|
|
20
|
+
title: '采购申请审批',
|
|
21
|
+
status: completed ? 'completed' : 'assigned',
|
|
22
|
+
approvalMode: 'single',
|
|
23
|
+
taskKind: 'normal',
|
|
24
|
+
assignedRoleSubjectKey: 'membership:manager',
|
|
25
|
+
activeParticipantId: 'participant-1',
|
|
26
|
+
participants: [],
|
|
27
|
+
originTaskId: null,
|
|
28
|
+
returnSessionId: null,
|
|
29
|
+
version: completed ? 2 : 1,
|
|
30
|
+
dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
|
|
31
|
+
createdAt: '2026-08-25T01:00:00.000Z',
|
|
32
|
+
updatedAt: '2026-08-25T01:10:00.000Z',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function surface(completed = false) {
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: 'openxiangda.workflow-surface/v2',
|
|
39
|
+
protocolVersion: 'workflow_surface_v2',
|
|
40
|
+
surfaceRevision: completed ? 'revision-2' : 'revision-1',
|
|
41
|
+
engineVersion: '2.0',
|
|
42
|
+
instanceSequence: completed ? 4 : 3,
|
|
43
|
+
navigationTarget: {
|
|
44
|
+
kind: 'resource_record',
|
|
45
|
+
appCode,
|
|
46
|
+
environmentKey: 'preproduction',
|
|
47
|
+
resourceCode: 'purchase-orders',
|
|
48
|
+
recordId: 'purchase-1',
|
|
49
|
+
desktopPath: '/purchase-orders/purchase-1',
|
|
50
|
+
mobilePath: '/m/purchase-orders/purchase-1',
|
|
51
|
+
},
|
|
52
|
+
instance: {
|
|
53
|
+
schemaVersion: 'openxiangda.workflow-instance/v2',
|
|
54
|
+
engineVersion: '2.0',
|
|
55
|
+
id: instanceId,
|
|
56
|
+
appCode,
|
|
57
|
+
environmentId: 'preproduction-id',
|
|
58
|
+
environmentKey: 'preproduction',
|
|
59
|
+
workflowCode: 'purchase-approval',
|
|
60
|
+
definitionVersion: 1,
|
|
61
|
+
bindingVersion: 1,
|
|
62
|
+
businessKey: 'PO-2026-0825',
|
|
63
|
+
generation: 1,
|
|
64
|
+
status: completed ? 'approved' : 'running',
|
|
65
|
+
initiatorUserId: 'applicant-user',
|
|
66
|
+
initiatorAuthorizationDigest: null,
|
|
67
|
+
dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
|
|
68
|
+
dataRevision: '8',
|
|
69
|
+
currentNodeId: completed ? null : 'manager-review',
|
|
70
|
+
outcome: completed ? 'approved' : null,
|
|
71
|
+
version: completed ? 3 : 2,
|
|
72
|
+
eventSequence: completed ? 4 : 3,
|
|
73
|
+
startedAt: '2026-08-25T01:00:00.000Z',
|
|
74
|
+
completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
|
|
75
|
+
},
|
|
76
|
+
task: completed ? null : task(false),
|
|
77
|
+
presentation: {
|
|
78
|
+
status: { label: completed ? '已同意' : '待审批', tone: 'warning' },
|
|
79
|
+
layout: 'approval_detail',
|
|
80
|
+
businessData: {
|
|
81
|
+
申请金额: '¥28,600',
|
|
82
|
+
采购类型: '办公设备',
|
|
83
|
+
期望到货: '2026-09-05',
|
|
84
|
+
申请人: '张三',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
fieldPolicy: { default: 'readonly', fields: {} },
|
|
88
|
+
operations: completed
|
|
89
|
+
? []
|
|
90
|
+
: [
|
|
91
|
+
{
|
|
92
|
+
key: 'approve',
|
|
93
|
+
kind: 'workflow_command',
|
|
94
|
+
label: '同意',
|
|
95
|
+
placement: 'primary',
|
|
96
|
+
tone: 'primary',
|
|
97
|
+
visible: true,
|
|
98
|
+
enabled: true,
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
comment: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
title: '审批意见',
|
|
105
|
+
format: 'textarea',
|
|
106
|
+
maxLength: 500,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
uiSchema: {
|
|
111
|
+
confirmText: '确认同意',
|
|
112
|
+
confirmation: { description: '提交后流程继续流转。' },
|
|
113
|
+
},
|
|
114
|
+
execute: {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
href: '/workflow/approve',
|
|
117
|
+
idempotencyRequired: true,
|
|
118
|
+
},
|
|
119
|
+
refresh: ['surface', 'timeline', 'work_center'],
|
|
120
|
+
},
|
|
121
|
+
...['reject', 'return', 'transfer', 'add_assignee'].map((key) => ({
|
|
122
|
+
key,
|
|
123
|
+
kind: 'workflow_command',
|
|
124
|
+
label: {
|
|
125
|
+
reject: '拒绝',
|
|
126
|
+
return: '退回',
|
|
127
|
+
transfer: '转交',
|
|
128
|
+
add_assignee: '加签',
|
|
129
|
+
}[key],
|
|
130
|
+
placement: 'secondary',
|
|
131
|
+
tone: key === 'reject' ? 'danger' : 'neutral',
|
|
132
|
+
visible: true,
|
|
133
|
+
enabled: true,
|
|
134
|
+
inputSchema: { type: 'object', properties: {} },
|
|
135
|
+
uiSchema: { confirmText: `确认${key}` },
|
|
136
|
+
execute: {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
href: `/workflow/${key}`,
|
|
139
|
+
idempotencyRequired: true,
|
|
140
|
+
},
|
|
141
|
+
refresh: ['surface', 'timeline', 'work_center'],
|
|
142
|
+
})),
|
|
143
|
+
],
|
|
144
|
+
extensions: {},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function timeline(completed = false) {
|
|
149
|
+
return {
|
|
150
|
+
engineVersion: '2.0',
|
|
151
|
+
instanceId,
|
|
152
|
+
flow: [
|
|
153
|
+
{
|
|
154
|
+
key: 'start',
|
|
155
|
+
nodeId: null,
|
|
156
|
+
kind: 'start',
|
|
157
|
+
title: '张三',
|
|
158
|
+
status: 'completed',
|
|
159
|
+
startedAt: '2026-08-25T01:00:00.000Z',
|
|
160
|
+
completedAt: '2026-08-25T01:00:00.000Z',
|
|
161
|
+
assignees: [],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
key: 'manager',
|
|
165
|
+
nodeId: 'manager-review',
|
|
166
|
+
kind: 'approval',
|
|
167
|
+
title: '部门负责人',
|
|
168
|
+
status: completed ? 'completed' : 'active',
|
|
169
|
+
startedAt: '2026-08-25T01:01:00.000Z',
|
|
170
|
+
completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
|
|
171
|
+
assignees: [{ userId: 'acceptance-user', displayName: '李明' }],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
key: 'finance',
|
|
175
|
+
nodeId: 'finance-review',
|
|
176
|
+
kind: 'approval',
|
|
177
|
+
title: '财务负责人',
|
|
178
|
+
status: completed ? 'completed' : 'waiting',
|
|
179
|
+
startedAt: null,
|
|
180
|
+
completedAt: null,
|
|
181
|
+
assignees: [{ userId: 'finance-user', displayName: '财务负责人' }],
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
items: [],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function mockWorkflow(page: Page, options: { forbidden?: boolean } = {}) {
|
|
189
|
+
let completed = false;
|
|
190
|
+
await page.route(`**${runtimeBase}/**`, async (route) => {
|
|
191
|
+
if (route.request().resourceType() !== 'document') return route.continue();
|
|
192
|
+
const response = await route.fetch();
|
|
193
|
+
const body = (await response.text()).replace(
|
|
194
|
+
'<head>',
|
|
195
|
+
`<head><meta name="openxiangda-runtime-base" content="${runtimeBase}/" />` +
|
|
196
|
+
`<meta name="openxiangda-app-code" content="${appCode}" />` +
|
|
197
|
+
'<meta name="openxiangda-environment" content="preproduction" />',
|
|
198
|
+
);
|
|
199
|
+
await route.fulfill({ response, body });
|
|
200
|
+
});
|
|
201
|
+
await page.route('**/service/**', async (route) => {
|
|
202
|
+
const url = new URL(route.request().url());
|
|
203
|
+
const path = url.pathname;
|
|
204
|
+
if (path.endsWith('/native/authz/current')) {
|
|
205
|
+
return route.fulfill({
|
|
206
|
+
contentType: 'application/json',
|
|
207
|
+
body: JSON.stringify(
|
|
208
|
+
envelope({
|
|
209
|
+
schemaVersion: 'openxiangda.runtime-authorization/v2',
|
|
210
|
+
state: 'active',
|
|
211
|
+
environment: {
|
|
212
|
+
id: 'preproduction-id',
|
|
213
|
+
key: 'preproduction',
|
|
214
|
+
activeAppVersionId: 'version-1',
|
|
215
|
+
headRevision: 1,
|
|
216
|
+
authzRevisionId: 'authz-1',
|
|
217
|
+
authzVersion: 1,
|
|
218
|
+
scopeDataVersion: 'scope-1',
|
|
219
|
+
},
|
|
220
|
+
principal: {
|
|
221
|
+
type: 'user_union',
|
|
222
|
+
userId: 'acceptance-user',
|
|
223
|
+
roleCodes: ['department_manager'],
|
|
224
|
+
capabilityCodes: [],
|
|
225
|
+
isAppSuperAdmin: false,
|
|
226
|
+
identityScope:
|
|
227
|
+
'user-union:preproduction-id:acceptance-user:authz-1',
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
230
|
+
),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (path.includes('/directory/')) {
|
|
234
|
+
return route.fulfill({
|
|
235
|
+
contentType: 'application/json',
|
|
236
|
+
body: JSON.stringify(envelope({ items: [], nextCursor: null })),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (options.forbidden && path.includes('/workflow/')) {
|
|
240
|
+
return route.fulfill({
|
|
241
|
+
status: 403,
|
|
242
|
+
contentType: 'application/json',
|
|
243
|
+
body: JSON.stringify(envelope(null, 403)),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (path.endsWith('/workflow/work-center/items')) {
|
|
247
|
+
return route.fulfill({
|
|
248
|
+
contentType: 'application/json',
|
|
249
|
+
body: JSON.stringify(
|
|
250
|
+
envelope({
|
|
251
|
+
engineVersion: '2.0',
|
|
252
|
+
authorizationMode: 'current_user_role_union',
|
|
253
|
+
authorizationDigest: 'digest-1',
|
|
254
|
+
status: 'pending',
|
|
255
|
+
total: 1,
|
|
256
|
+
limit: 20,
|
|
257
|
+
offset: 0,
|
|
258
|
+
items: [task(completed)],
|
|
259
|
+
}),
|
|
260
|
+
),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (path.endsWith(`/workflow/tasks/${taskId}/surface`)) {
|
|
264
|
+
return route.fulfill({
|
|
265
|
+
contentType: 'application/json',
|
|
266
|
+
body: JSON.stringify(envelope(surface(completed))),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (path.endsWith(`/workflow/instances/${instanceId}/timeline`)) {
|
|
270
|
+
return route.fulfill({
|
|
271
|
+
contentType: 'application/json',
|
|
272
|
+
body: JSON.stringify(envelope(timeline(completed))),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (path.endsWith(`/workflow/tasks/${taskId}/commands/approve`)) {
|
|
276
|
+
completed = true;
|
|
277
|
+
return route.fulfill({
|
|
278
|
+
contentType: 'application/json',
|
|
279
|
+
body: JSON.stringify(envelope({ status: 'completed' })),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return route.fulfill({
|
|
283
|
+
status: 404,
|
|
284
|
+
contentType: 'application/json',
|
|
285
|
+
body: JSON.stringify(envelope(null, 404)),
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
test('renders a paged desktop work center from the current-user role union', async ({
|
|
291
|
+
page,
|
|
292
|
+
}) => {
|
|
293
|
+
await mockWorkflow(page);
|
|
294
|
+
await page.goto(`${runtimeBase}/admin/work-center`);
|
|
295
|
+
await expect(page.getByRole('heading', { name: '审批工作台' })).toBeVisible();
|
|
296
|
+
await expect(page.getByRole('link', { name: '采购申请审批' })).toBeVisible();
|
|
297
|
+
await expect(page.getByText('PO-2026-0825')).toBeVisible();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('renders the independent mobile task and executes only a Surface operation', async ({
|
|
301
|
+
page,
|
|
302
|
+
}) => {
|
|
303
|
+
await mockWorkflow(page);
|
|
304
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
305
|
+
await page.goto(`${runtimeBase}/m/tasks/${taskId}`);
|
|
306
|
+
await expect(page.locator('.oxa-workflow-mobile-page')).toBeVisible();
|
|
307
|
+
await expect(page.getByText('¥28,600')).toBeVisible();
|
|
308
|
+
await expect(page.getByText('财务负责人').first()).toBeVisible();
|
|
309
|
+
await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
|
|
310
|
+
await page.getByRole('button', { name: /同\s*意/ }).click();
|
|
311
|
+
await page.getByLabel('审批意见').fill('同意采购');
|
|
312
|
+
await page.getByRole('button', { name: '确认同意' }).click();
|
|
313
|
+
await expect(page.getByText('同意已提交')).toBeVisible();
|
|
314
|
+
await expect(page.getByRole('button', { name: /同\s*意/ })).toHaveCount(0);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('shows an explicit authorization error instead of an empty work center', async ({
|
|
318
|
+
page,
|
|
319
|
+
}) => {
|
|
320
|
+
test.setTimeout(45_000);
|
|
321
|
+
await mockWorkflow(page, { forbidden: true });
|
|
322
|
+
await page.goto(`${runtimeBase}/admin/work-center`, {
|
|
323
|
+
waitUntil: 'domcontentloaded',
|
|
324
|
+
});
|
|
325
|
+
await expect(page.getByRole('alert')).toContainText('HTTP_403: forbidden', {
|
|
326
|
+
timeout: 30_000,
|
|
327
|
+
});
|
|
328
|
+
await expect(page.getByRole('link', { name: '采购申请审批' })).toHaveCount(0);
|
|
329
|
+
});
|
|
@@ -1,32 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { gzipSync } from 'node:zlib';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
1
|
+
import { verifyOpenXiangdaWebBuild } from 'openxiangda/testing';
|
|
4
2
|
|
|
5
|
-
const
|
|
6
|
-
const assets = join(dist, 'assets');
|
|
7
|
-
const names = readdirSync(assets);
|
|
8
|
-
const files = names.map(name => join(assets, name));
|
|
9
|
-
const total = files.reduce((bytes, file) => bytes + statSync(file).size, 0);
|
|
10
|
-
if (total > 2_500_000) throw new Error(`WEB_DIST_BUDGET_EXCEEDED:${total}>2500000`);
|
|
11
|
-
const javascript = files.filter(file => file.endsWith('.js'));
|
|
12
|
-
const gzip = javascript.reduce(
|
|
13
|
-
(bytes, file) => bytes + gzipSync(readFileSync(file), { level: 9 }).byteLength,
|
|
14
|
-
0
|
|
15
|
-
);
|
|
16
|
-
if (gzip > 800_000) throw new Error(`WEB_JS_GZIP_BUDGET_EXCEEDED:${gzip}>800000`);
|
|
17
|
-
if (names.some(name => name.endsWith('.map'))) throw new Error('WEB_SOURCE_MAP_FORBIDDEN');
|
|
18
|
-
const html = readFileSync(join(dist, 'index.html'), 'utf8');
|
|
19
|
-
if (!html.includes('<div id="root"></div>')) throw new Error('WEB_ROOT_MISSING');
|
|
20
|
-
const initialNames = [
|
|
21
|
-
...html.matchAll(/(?:src|href)="\.\/assets\/([^"]+\.js)"/g),
|
|
22
|
-
].map(match => match[1]);
|
|
23
|
-
const initialGzip = [...new Set(initialNames)].reduce((bytes, name) => {
|
|
24
|
-
const file = join(assets, name);
|
|
25
|
-
return bytes + gzipSync(readFileSync(file), { level: 9 }).byteLength;
|
|
26
|
-
}, 0);
|
|
27
|
-
if (initialGzip > 600_000) {
|
|
28
|
-
throw new Error(`WEB_INITIAL_JS_GZIP_BUDGET_EXCEEDED:${initialGzip}>600000`);
|
|
29
|
-
}
|
|
3
|
+
const result = verifyOpenXiangdaWebBuild(new URL('../dist', import.meta.url));
|
|
30
4
|
process.stdout.write(
|
|
31
|
-
`Verified Vite build: ${
|
|
5
|
+
`Verified Vite build: ${result.totalBytes} bytes, ${result.totalJavaScriptGzipBytes} total gzip bytes, ${result.initialJavaScriptGzipBytes} initial gzip bytes.\n`
|
|
32
6
|
);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
appCode,
|
|
6
6
|
appName,
|
|
7
7
|
resourceDefinitions,
|
|
8
|
+
workflowCodes,
|
|
8
9
|
} from '../../../packages/contracts/src/generated.js';
|
|
9
10
|
import 'openxiangda/react/styles.css';
|
|
10
11
|
|
|
@@ -14,6 +15,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
14
15
|
appCode={appCode}
|
|
15
16
|
appName={appName}
|
|
16
17
|
resourceDefinitions={resourceDefinitions}
|
|
18
|
+
workflows={workflowCodes}
|
|
17
19
|
/>
|
|
18
20
|
</React.StrictMode>,
|
|
19
21
|
);
|
|
@@ -70,3 +70,30 @@ test('runtime metadata stays mount-scoped', () => {
|
|
|
70
70
|
);
|
|
71
71
|
assert.equal(resolveApplicationBasename(mount), '/view/example-app');
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
test('resource E2E harness owns the standard export download response', () => {
|
|
75
|
+
const source = readFileSync(
|
|
76
|
+
new URL('../e2e/resources.spec.ts', import.meta.url),
|
|
77
|
+
'utf8'
|
|
78
|
+
);
|
|
79
|
+
assert.match(source, /\/native\\\/data\\\/\(\[\^\/\]\+\)\\\/export\$/);
|
|
80
|
+
assert.match(source, /content-disposition/);
|
|
81
|
+
assert.match(source, /text\/csv/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('published E2E harnesses do not write evidence into the platform source repository', () => {
|
|
85
|
+
const workflowSource = readFileSync(
|
|
86
|
+
new URL('../e2e/workflow.spec.ts', import.meta.url),
|
|
87
|
+
'utf8'
|
|
88
|
+
);
|
|
89
|
+
assert.doesNotMatch(workflowSource, /docs\/architecture|evidenceDirectory/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('the platform package owns Web build verification policy', () => {
|
|
93
|
+
const verifier = readFileSync(
|
|
94
|
+
new URL('../scripts/verify-build.mjs', import.meta.url),
|
|
95
|
+
'utf8'
|
|
96
|
+
);
|
|
97
|
+
assert.match(verifier, /verifyOpenXiangdaWebBuild/);
|
|
98
|
+
assert.doesNotMatch(verifier, /2_650_000|800_000|600_000/);
|
|
99
|
+
});
|