openxiangda-cli 2.0.0-alpha.85 → 2.0.0-alpha.89
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/package.json +4 -4
- package/template/AGENTS.md +4 -4
- package/template/README.md +4 -4
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +14 -1
- package/template/apps/server/test/smoke.test.ts +3 -1
- package/template/apps/web/data-api-live.e2e.html +15 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +141 -0
- package/template/apps/web/e2e/data-api-live.spec.ts +138 -0
- package/template/apps/web/e2e/field-protocol-fixture.tsx +280 -0
- package/template/apps/web/e2e/field-protocol.spec.ts +100 -0
- package/template/apps/web/e2e/resources.spec.ts +258 -20
- package/template/apps/web/field-protocol.e2e.html +12 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/playwright.config.ts +3 -1
- package/template/apps/web/scripts/check.mjs +8 -5
- package/template/apps/web/scripts/verify-build.mjs +1 -1
- package/template/apps/web/src/AuthoritativeSelector.tsx +165 -132
- package/template/apps/web/src/FilePreviewPage.tsx +30 -9
- package/template/apps/web/src/RoleSubjectSelect.tsx +128 -0
- package/template/apps/web/src/Shell.tsx +93 -6
- package/template/apps/web/src/components/platform-fields/AddressField.tsx +338 -0
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +25 -6
- package/template/apps/web/src/components/platform-fields/CascadeField.tsx +49 -0
- package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +148 -0
- package/template/apps/web/src/components/platform-fields/JsonField.tsx +77 -0
- package/template/apps/web/src/components/platform-fields/LocationField.tsx +164 -0
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +19 -44
- package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +67 -0
- package/template/apps/web/src/components/platform-fields/RichTextField.tsx +196 -0
- package/template/apps/web/src/components/platform-fields/SignatureField.tsx +315 -0
- package/template/apps/web/src/components/platform-fields/SubtableField.tsx +553 -0
- package/template/apps/web/src/components/platform-fields/address-value.ts +80 -0
- package/template/apps/web/src/components/platform-fields/cascade-value.ts +66 -0
- package/template/apps/web/src/components/platform-fields/directory-value.ts +53 -0
- package/template/apps/web/src/components/platform-fields/field-form-codec.ts +98 -0
- package/template/apps/web/src/components/platform-fields/location-value.ts +87 -0
- package/template/apps/web/src/components/platform-fields/resource-query.ts +131 -0
- package/template/apps/web/src/components/platform-fields/rich-text-value.ts +59 -0
- package/template/apps/web/src/components/platform-fields/subtable-value.ts +187 -0
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +194 -108
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +8 -1
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +385 -79
- package/template/apps/web/src/components/resource/generated-resource-definition.ts +18 -0
- package/template/apps/web/src/components/resource/resource-import.ts +4 -4
- package/template/apps/web/src/data-provider.ts +59 -34
- package/template/apps/web/src/platform-client.ts +341 -136
- package/template/apps/web/src/runtime.tsx +156 -17
- package/template/apps/web/src/styles.css +264 -0
- package/template/apps/web/test/address-field.test.ts +66 -0
- package/template/apps/web/test/cascade-value.test.ts +68 -0
- package/template/apps/web/test/contracts.test.ts +29 -5
- package/template/apps/web/test/directory-value.test.ts +55 -0
- package/template/apps/web/test/field-bounds.test.ts +17 -0
- package/template/apps/web/test/field-form-codec.test.ts +85 -0
- package/template/apps/web/test/location-field.test.ts +83 -0
- package/template/apps/web/test/resource-import.test.ts +8 -7
- package/template/apps/web/test/resource-query.test.ts +144 -0
- package/template/apps/web/test/rich-json-field.test.ts +53 -0
- package/template/apps/web/test/signature-serial-field.test.ts +40 -0
- package/template/apps/web/test/subtable-value.test.ts +144 -0
- package/template/openxiangda.config.ts +1 -1
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +95 -0
- package/template/scripts/verify-template-budget.mjs +11 -4
|
@@ -21,11 +21,132 @@ const definitions = resourceDefinitions as Record<string, DeclaredResource>;
|
|
|
21
21
|
const allCapabilities = [...capabilities] as string[];
|
|
22
22
|
const runtimeBase = `/view/${appCode}`;
|
|
23
23
|
|
|
24
|
+
const roleSubjects = [
|
|
25
|
+
{
|
|
26
|
+
subjectKey: 'membership:college-admin',
|
|
27
|
+
subjectKind: 'membership',
|
|
28
|
+
role: {
|
|
29
|
+
code: 'college_admin',
|
|
30
|
+
name: '学院管理员',
|
|
31
|
+
source: 'package',
|
|
32
|
+
description: '管理本学院业务数据',
|
|
33
|
+
},
|
|
34
|
+
revision: 1,
|
|
35
|
+
scopeDimensionCount: 1,
|
|
36
|
+
scopeSummary: [],
|
|
37
|
+
scopeSummaryTruncated: false,
|
|
38
|
+
validFrom: null,
|
|
39
|
+
validTo: null,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
subjectKey: 'membership:instrument-admin',
|
|
43
|
+
subjectKind: 'membership',
|
|
44
|
+
role: {
|
|
45
|
+
code: 'instrument_admin',
|
|
46
|
+
name: '仪器管理员',
|
|
47
|
+
source: 'package',
|
|
48
|
+
description: '管理分配的仪器',
|
|
49
|
+
},
|
|
50
|
+
revision: 1,
|
|
51
|
+
scopeDimensionCount: 0,
|
|
52
|
+
scopeSummary: [],
|
|
53
|
+
scopeSummaryTruncated: false,
|
|
54
|
+
validFrom: null,
|
|
55
|
+
validTo: null,
|
|
56
|
+
},
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
function roleContext(subjectKey: string, authorized: boolean) {
|
|
60
|
+
const subject = roleSubjects.find(item => item.subjectKey === subjectKey)!;
|
|
61
|
+
const roleSessionId = `role-session-${subject.role.code}`;
|
|
62
|
+
return {
|
|
63
|
+
schemaVersion: 'openxiangda.role-session-context/v2',
|
|
64
|
+
state: 'active',
|
|
65
|
+
environment: {
|
|
66
|
+
id: 'preproduction-id',
|
|
67
|
+
key: 'preproduction',
|
|
68
|
+
kind: 'preproduction',
|
|
69
|
+
activeAppVersionId: 'app-version-1',
|
|
70
|
+
headRevision: 1,
|
|
71
|
+
authzVersion: 1,
|
|
72
|
+
roleSubjectSetVersion: 'subjects-1',
|
|
73
|
+
},
|
|
74
|
+
identityScope: `preproduction-id:${subjectKey}:1`,
|
|
75
|
+
subjectProfile: {
|
|
76
|
+
schemaVersion: 'openxiangda.subject-profile/v2',
|
|
77
|
+
userId: 'acceptance-user',
|
|
78
|
+
displayName: '验收用户',
|
|
79
|
+
avatarUrl: null,
|
|
80
|
+
jobNumber: null,
|
|
81
|
+
affiliatedDepartment: { id: 'department-1', name: '实验中心' },
|
|
82
|
+
},
|
|
83
|
+
currentRoleSubject: subject,
|
|
84
|
+
roleSubjectPage: {
|
|
85
|
+
schemaVersion: 'openxiangda.role-subject-page/v2',
|
|
86
|
+
items: roleSubjects,
|
|
87
|
+
total: roleSubjects.length,
|
|
88
|
+
nextCursor: null,
|
|
89
|
+
roleSubjectSetVersion: 'subjects-1',
|
|
90
|
+
},
|
|
91
|
+
roleSession: {
|
|
92
|
+
schemaVersion: 'openxiangda.native-role-session/v2',
|
|
93
|
+
id: roleSessionId,
|
|
94
|
+
environmentId: 'preproduction-id',
|
|
95
|
+
environmentKey: 'preproduction',
|
|
96
|
+
loginSessionId: 'login-session-1',
|
|
97
|
+
userId: 'acceptance-user',
|
|
98
|
+
selectionKey: subjectKey,
|
|
99
|
+
subjectKind: 'membership',
|
|
100
|
+
activeRoleCode: subject.role.code,
|
|
101
|
+
activeRoleSource: 'package',
|
|
102
|
+
activeRoleMembershipId: subjectKey.slice('membership:'.length),
|
|
103
|
+
activeAuthzRevisionId: 'authz-1',
|
|
104
|
+
activeAuthzVersion: 1,
|
|
105
|
+
activeAppVersionId: 'app-version-1',
|
|
106
|
+
activeEnvironmentHeadRevision: 1,
|
|
107
|
+
activeMembershipRevision: 1,
|
|
108
|
+
status: 'active',
|
|
109
|
+
issuedAt: '2026-08-23T00:00:00.000Z',
|
|
110
|
+
expiresAt: '2099-08-23T00:00:00.000Z',
|
|
111
|
+
lastUsedAt: '2026-08-23T00:00:00.000Z',
|
|
112
|
+
},
|
|
113
|
+
principal: {
|
|
114
|
+
schemaVersion: 'openxiangda.native-principal/v2',
|
|
115
|
+
tenantId: 'tenant-1',
|
|
116
|
+
appCode,
|
|
117
|
+
environmentId: 'preproduction-id',
|
|
118
|
+
environmentKey: 'preproduction',
|
|
119
|
+
activeAppVersionId: 'app-version-1',
|
|
120
|
+
environmentHeadRevision: 1,
|
|
121
|
+
principalType: 'user',
|
|
122
|
+
subjectId: 'acceptance-user',
|
|
123
|
+
userId: 'acceptance-user',
|
|
124
|
+
loginSessionId: 'login-session-1',
|
|
125
|
+
roleSessionId,
|
|
126
|
+
subjectKind: 'membership',
|
|
127
|
+
activeRoleCode: subject.role.code,
|
|
128
|
+
activeRoleSource: 'package',
|
|
129
|
+
activeRoleMembershipId: subjectKey.slice('membership:'.length),
|
|
130
|
+
activeMembershipRevision: 1,
|
|
131
|
+
authzRevisionId: 'authz-1',
|
|
132
|
+
authzVersion: 1,
|
|
133
|
+
scopeDataVersion: 'scope-1',
|
|
134
|
+
isAppSuperAdmin: false,
|
|
135
|
+
capabilities: authorized ? allCapabilities : [],
|
|
136
|
+
expiresAt: null,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
24
141
|
async function mockPlatform(
|
|
25
142
|
page: Page,
|
|
26
143
|
authorized = true,
|
|
27
144
|
targetRuntimeBase = runtimeBase,
|
|
28
145
|
) {
|
|
146
|
+
let activeSubjectKey = roleSubjects[0].subjectKey;
|
|
147
|
+
let contextReads = 0;
|
|
148
|
+
const observed: Array<{ path: string; roleSessionId: string }> = [];
|
|
149
|
+
const switches: Array<Record<string, unknown>> = [];
|
|
29
150
|
await page.route(`**${targetRuntimeBase}/**`, async route => {
|
|
30
151
|
if (route.request().resourceType() !== 'document') return route.continue();
|
|
31
152
|
const response = await route.fetch();
|
|
@@ -39,36 +160,69 @@ async function mockPlatform(
|
|
|
39
160
|
});
|
|
40
161
|
await page.route('**/service/**', async route => {
|
|
41
162
|
const url = new URL(route.request().url());
|
|
42
|
-
|
|
163
|
+
const request = route.request();
|
|
164
|
+
const roleSessionId = request.headers()['x-openxiangda-role-session-id'] || '';
|
|
165
|
+
if (url.pathname.endsWith('/native/authz/role-session/context/role-subjects')) {
|
|
166
|
+
return route.fulfill({
|
|
167
|
+
contentType: 'application/json',
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
code: 200,
|
|
170
|
+
data: {
|
|
171
|
+
schemaVersion: 'openxiangda.role-subject-page/v2',
|
|
172
|
+
items: roleSubjects,
|
|
173
|
+
total: roleSubjects.length,
|
|
174
|
+
nextCursor: null,
|
|
175
|
+
roleSubjectSetVersion: 'subjects-1',
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (url.pathname.endsWith('/native/authz/role-session/context/switch')) {
|
|
181
|
+
const body = request.postDataJSON() as Record<string, unknown>;
|
|
182
|
+
switches.push(body);
|
|
183
|
+
if (roleSubjects.some(item => item.subjectKey === body.targetRoleSubjectKey)) {
|
|
184
|
+
activeSubjectKey = String(body.targetRoleSubjectKey) as typeof activeSubjectKey;
|
|
185
|
+
}
|
|
186
|
+
return route.fulfill({
|
|
187
|
+
contentType: 'application/json',
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
code: 200,
|
|
190
|
+
data: roleContext(activeSubjectKey, authorized),
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (url.pathname.endsWith('/native/authz/role-session/context')) {
|
|
195
|
+
contextReads += 1;
|
|
196
|
+
return route.fulfill({
|
|
197
|
+
contentType: 'application/json',
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
code: 200,
|
|
200
|
+
data: roleContext(activeSubjectKey, authorized),
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const sourceQuery = url.pathname.match(
|
|
205
|
+
/\/native\/data-resources\/([^/]+)\/fields\/([^/]+)\/source\/query$/
|
|
206
|
+
);
|
|
207
|
+
if (sourceQuery) {
|
|
208
|
+
observed.push({ path: url.pathname, roleSessionId });
|
|
43
209
|
return route.fulfill({
|
|
44
210
|
contentType: 'application/json',
|
|
45
211
|
body: JSON.stringify({
|
|
46
212
|
code: 200,
|
|
47
213
|
data: {
|
|
48
|
-
schemaVersion: 'openxiangda.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
activeAppVersionId: 'app-version-1',
|
|
54
|
-
headRevision: 1,
|
|
55
|
-
authzRevisionId: 'authz-1',
|
|
56
|
-
authzVersion: 1,
|
|
57
|
-
scopeDataVersion: 'scope-1',
|
|
58
|
-
},
|
|
59
|
-
principal: {
|
|
60
|
-
type: 'user',
|
|
61
|
-
userId: 'acceptance-user',
|
|
62
|
-
roleCodes: [],
|
|
63
|
-
capabilityCodes: authorized ? allCapabilities : [],
|
|
64
|
-
isAppSuperAdmin: authorized,
|
|
65
|
-
},
|
|
214
|
+
schemaVersion: 'openxiangda.data-field-source-page/v2',
|
|
215
|
+
resourceCode: decodeURIComponent(sourceQuery[1]),
|
|
216
|
+
fieldCode: decodeURIComponent(sourceQuery[2]),
|
|
217
|
+
items: [],
|
|
218
|
+
nextCursor: null,
|
|
66
219
|
},
|
|
67
220
|
}),
|
|
68
221
|
});
|
|
69
222
|
}
|
|
70
223
|
const query = url.pathname.match(/\/native\/data\/([^/]+)\/query$/);
|
|
71
224
|
if (query) {
|
|
225
|
+
observed.push({ path: url.pathname, roleSessionId });
|
|
72
226
|
return route.fulfill({
|
|
73
227
|
contentType: 'application/json',
|
|
74
228
|
body: JSON.stringify({
|
|
@@ -86,18 +240,33 @@ async function mockPlatform(
|
|
|
86
240
|
}),
|
|
87
241
|
});
|
|
88
242
|
}
|
|
89
|
-
if (url.pathname.includes('/
|
|
243
|
+
if (url.pathname.includes('/directory/')) {
|
|
244
|
+
observed.push({ path: url.pathname, roleSessionId });
|
|
90
245
|
return route.fulfill({
|
|
91
246
|
contentType: 'application/json',
|
|
92
247
|
body: JSON.stringify({ code: 200, data: { items: [], nextCursor: null } }),
|
|
93
248
|
});
|
|
94
249
|
}
|
|
250
|
+
if (url.pathname.includes('/openxiangda-app-api/v2/')) {
|
|
251
|
+
observed.push({ path: url.pathname, roleSessionId });
|
|
252
|
+
return route.fulfill({
|
|
253
|
+
contentType: 'application/json',
|
|
254
|
+
body: JSON.stringify({ code: 200, data: { status: 'ok' } }),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
95
257
|
return route.fulfill({
|
|
96
258
|
status: 404,
|
|
97
259
|
contentType: 'application/json',
|
|
98
260
|
body: JSON.stringify({ code: 404, message: 'not mocked' }),
|
|
99
261
|
});
|
|
100
262
|
});
|
|
263
|
+
return {
|
|
264
|
+
observed,
|
|
265
|
+
switches,
|
|
266
|
+
get contextReads() {
|
|
267
|
+
return contextReads;
|
|
268
|
+
},
|
|
269
|
+
};
|
|
101
270
|
}
|
|
102
271
|
|
|
103
272
|
test('renders the compiled desktop resource contract and shared workbench', async ({ page }) => {
|
|
@@ -145,6 +314,75 @@ test('opens the stable admin entry under the preproduction gateway mount', async
|
|
|
145
314
|
await expect(page).toHaveURL(`${devRuntimeBase}/${codes[0]}`);
|
|
146
315
|
});
|
|
147
316
|
|
|
317
|
+
test('binds Data, Directory, App API, and role switching to one active RoleSession', async ({ page }) => {
|
|
318
|
+
const platform = await mockPlatform(page);
|
|
319
|
+
await page.goto(`${runtimeBase}/admin`);
|
|
320
|
+
if (codes.length) {
|
|
321
|
+
await expect(page.locator('.oxa-list-surface')).toBeVisible();
|
|
322
|
+
} else {
|
|
323
|
+
await expect(page.getByText('还没有声明数据资源')).toBeVisible();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await page.evaluate(async () => {
|
|
327
|
+
const client = await import('/src/platform-client.ts');
|
|
328
|
+
await client.searchResource('acceptance-resource', 'name', {
|
|
329
|
+
keyword: '显微镜',
|
|
330
|
+
});
|
|
331
|
+
await client.searchDirectory('user', { keyword: '老师' });
|
|
332
|
+
await client.requestApplicationApi('/health/current-user');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const initialSession = 'role-session-college_admin';
|
|
336
|
+
expect(
|
|
337
|
+
platform.observed.filter(item =>
|
|
338
|
+
/\/native\/data(?:-resources)?\/|\/directory\/|\/openxiangda-app-api\/v2\//.test(
|
|
339
|
+
item.path
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
).toEqual(
|
|
343
|
+
expect.arrayContaining([
|
|
344
|
+
expect.objectContaining({ roleSessionId: initialSession }),
|
|
345
|
+
expect.objectContaining({ roleSessionId: initialSession }),
|
|
346
|
+
expect.objectContaining({ roleSessionId: initialSession }),
|
|
347
|
+
])
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
await page.locator('.oxa-current-user').click();
|
|
351
|
+
await page.getByText('切换应用角色', { exact: true }).click();
|
|
352
|
+
await page.getByRole('combobox', { name: '选择应用角色' }).click();
|
|
353
|
+
await page.getByText('仪器管理员', { exact: true }).click();
|
|
354
|
+
await page.getByRole('button', { name: '切换角色' }).click();
|
|
355
|
+
|
|
356
|
+
await expect.poll(() => platform.switches.length).toBe(1);
|
|
357
|
+
expect(platform.switches[0]).toMatchObject({
|
|
358
|
+
targetRoleSubjectKey: 'membership:instrument-admin',
|
|
359
|
+
expectedRoleSessionId: initialSession,
|
|
360
|
+
environmentKey: 'preproduction',
|
|
361
|
+
});
|
|
362
|
+
await expect.poll(() => platform.contextReads).toBeGreaterThan(1);
|
|
363
|
+
await page.locator('.oxa-current-user').click();
|
|
364
|
+
await expect(page.locator('.oxa-user-dropdown-roles')).toContainText(
|
|
365
|
+
'仪器管理员'
|
|
366
|
+
);
|
|
367
|
+
await page.keyboard.press('Escape');
|
|
368
|
+
await page.evaluate(async () => {
|
|
369
|
+
const client = await import('/src/platform-client.ts');
|
|
370
|
+
await client.searchResource('acceptance-resource', 'name', {
|
|
371
|
+
keyword: '光谱仪',
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
await expect
|
|
375
|
+
.poll(
|
|
376
|
+
() =>
|
|
377
|
+
platform.observed.filter(
|
|
378
|
+
item =>
|
|
379
|
+
item.path.includes('/native/data-resources/') &&
|
|
380
|
+
item.roleSessionId === 'role-session-instrument_admin'
|
|
381
|
+
).length
|
|
382
|
+
)
|
|
383
|
+
.toBeGreaterThan(0);
|
|
384
|
+
});
|
|
385
|
+
|
|
148
386
|
test('renders platform mobile controls from declared field widgets', async ({ page }) => {
|
|
149
387
|
const candidate = codes
|
|
150
388
|
.map(code => ({ code, definition: definitions[code] }))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Field protocol acceptance</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/e2e/field-protocol-fixture.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -16,7 +16,9 @@ export default defineConfig({
|
|
|
16
16
|
env: {
|
|
17
17
|
OPENXIANGDA_WEB_PORT: String(e2ePort),
|
|
18
18
|
OPENXIANGDA_ENVIRONMENT_KEY: 'production',
|
|
19
|
-
OPENXIANGDA_DEV_PROXY:
|
|
19
|
+
OPENXIANGDA_DEV_PROXY:
|
|
20
|
+
process.env.OPENXIANGDA_E2E_PLATFORM_URL ||
|
|
21
|
+
'http://127.0.0.1:7001',
|
|
20
22
|
},
|
|
21
23
|
},
|
|
22
24
|
});
|
|
@@ -14,10 +14,11 @@ visit(join(root, 'src'));
|
|
|
14
14
|
const lines = files.reduce((total, file) => total + readFileSync(file, 'utf8').split(/\r?\n/).length, 0);
|
|
15
15
|
// The shared Surface renderer is deliberately counted as application source;
|
|
16
16
|
// keep its small budget explicit instead of hiding it in generated output.
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
if (
|
|
17
|
+
// Field Kit families are separate modules so a semantic field does not add a
|
|
18
|
+
// branch to one universal renderer. Keep explicit ceilings while allowing the
|
|
19
|
+
// complete 30-field standard kit to remain decomposed.
|
|
20
|
+
if (files.length > 36) throw new Error(`WEB_SOURCE_FILE_BUDGET_EXCEEDED:${files.length}>36`);
|
|
21
|
+
if (lines > 10_000) throw new Error(`WEB_BUSINESS_LOC_BUDGET_EXCEEDED:${lines}>10000`);
|
|
21
22
|
const source = files.map((file) => readFileSync(file, 'utf8')).join('\n');
|
|
22
23
|
for (const marker of [
|
|
23
24
|
'@umijs/',
|
|
@@ -26,7 +27,9 @@ for (const marker of [
|
|
|
26
27
|
'instrument_query',
|
|
27
28
|
'instrument_save',
|
|
28
29
|
'function.invoke',
|
|
29
|
-
'
|
|
30
|
+
'authorization' + '-mode',
|
|
31
|
+
'focused' + '_role',
|
|
32
|
+
'/native/current' + '-user',
|
|
30
33
|
'college-' + 'am',
|
|
31
34
|
'user-' + 'wang',
|
|
32
35
|
'dept-' + 'am-test',
|
|
@@ -13,7 +13,7 @@ const gzip = javascript.reduce(
|
|
|
13
13
|
(bytes, file) => bytes + gzipSync(readFileSync(file), { level: 9 }).byteLength,
|
|
14
14
|
0
|
|
15
15
|
);
|
|
16
|
-
if (gzip >
|
|
16
|
+
if (gzip > 800_000) throw new Error(`WEB_JS_GZIP_BUDGET_EXCEEDED:${gzip}>800000`);
|
|
17
17
|
if (names.some(name => name.endsWith('.map'))) throw new Error('WEB_SOURCE_MAP_FORBIDDEN');
|
|
18
18
|
const html = readFileSync(join(dist, 'index.html'), 'utf8');
|
|
19
19
|
if (!html.includes('<div id="root"></div>')) throw new Error('WEB_ROOT_MISSING');
|