@mingfuyun/star-allinone 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +35 -75
  2. package/dist/components/CustomDrawer/index.d.ts +2 -0
  3. package/dist/components/CustomDrawer/index.d.ts.map +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.esm.js +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/tracking/index.d.ts +60 -0
  9. package/dist/tracking/index.d.ts.map +1 -0
  10. package/examples/AGENTS.md +14 -2
  11. package/package.json +4 -2
  12. package/scripts/cli.js +28 -1
  13. package/scripts/gen/doctor.js +293 -0
  14. package/scripts/gen/fields.js +118 -0
  15. package/scripts/gen/fixtures/run.js +78 -10
  16. package/scripts/gen/fixtures/widgets.openapi.json +2 -1
  17. package/scripts/gen/page.js +194 -43
  18. package/skills/README.md +16 -3
  19. package/skills/shared/design-to-page/SKILL.md +3 -3
  20. package/skills/shared/design-to-page/references/list-page-template.md +13 -3
  21. package/skills/shared/design-to-page/references/overlays.md +6 -0
  22. package/skills/shared/design-to-page/references/page-conventions.md +2 -1
  23. package/skills/shared/project-conventions/references/architecture.md +5 -2
  24. package/skills/shared/project-conventions/references/component-contract.md +38 -0
  25. package/skills/shared/project-conventions/references/mfprotable.md +59 -0
  26. package/skills/shared/project-conventions/references/quality-checklist.md +14 -0
  27. package/skills/shared/project-conventions/references/review-checklist.md +2 -2
  28. package/skills/shared/publish-test/SKILL.md +10 -0
  29. package/skills/shared/review-and-submit/references/ai-review.md +5 -1
  30. package/skills/shared/review-and-submit/references/verify.md +2 -0
@@ -1,8 +1,16 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { toPascal, parseOperationSelector } = require('./names');
3
+ const { toPascal } = require('./names');
4
4
  const { generateService } = require('./service');
5
5
  const { loadProject, writeFile } = require('./project');
6
+ const {
7
+ schemaFields,
8
+ formComponent,
9
+ columnValueType,
10
+ fallbackFields,
11
+ SKIP_FORM,
12
+ querySearchNames,
13
+ } = require('./fields');
6
14
 
7
15
  function detectLayout(root) {
8
16
  const hasPageContainer = fs.existsSync(path.join(root, 'src/components/PageContainer/index.tsx'));
@@ -28,30 +36,39 @@ function featureNames(options) {
28
36
  };
29
37
  }
30
38
 
31
- function columnFields(itemTypeSource = '') {
32
- const fields = [...itemTypeSource.matchAll(/^\s+(\w+)\??:/gm)].map((match) => match[1]);
33
- const preferred = fields.filter((name) => !['created_at', 'updated_at', 'createTime', 'updateTime'].includes(name));
34
- return (preferred.length ? preferred : fields).slice(0, 6);
39
+ function searchFieldNames(fields) {
40
+ return fields
41
+ .filter((field) => field.name !== 'id' && (field.type === 'string' || !field.type))
42
+ .slice(0, 2)
43
+ .map((field) => field.name);
35
44
  }
36
45
 
37
- function emitColumns(names, itemType, fields, permissions) {
38
- const columns = fields.map((field, index) => {
39
- const search = index < 2;
46
+ function emitColumns(names, itemType, fields, permissions, queryNames) {
47
+ const columnFields = fields.length ? fields : [{ name: 'id', title: 'ID', type: 'string' }];
48
+ const searchable = new Set(
49
+ queryNames?.length
50
+ ? columnFields.filter((field) => queryNames.includes(field.name)).map((field) => field.name)
51
+ : searchFieldNames(columnFields),
52
+ );
53
+ const columns = columnFields.map((field) => {
54
+ const search = searchable.has(field.name);
40
55
  return ` {
41
- title: '${field}',
42
- dataIndex: '${field}',
43
- key: '${field}',
56
+ title: ${JSON.stringify(field.title)},
57
+ dataIndex: '${field.name}',
58
+ key: '${field.name}',
44
59
  hideInSearch: ${search ? 'false' : 'true'},
45
- ellipsis: true,
46
- render: (_, record) => record.${field} ?? '-',
60
+ ellipsis: true,${valueTypeBlock(field)}${enumBlock(field)}
61
+ render: (_, record) => record.${field.name} ?? '-',
47
62
  }`;
48
63
  });
49
- if (!columns.length) {
64
+ for (const name of queryNames || []) {
65
+ if (columnFields.some((field) => field.name === name)) continue;
50
66
  columns.push(` {
51
- title: 'ID',
52
- dataIndex: 'id',
53
- key: 'id',
54
- hideInSearch: true,
67
+ title: ${JSON.stringify(name)},
68
+ dataIndex: '${name}',
69
+ key: '${name}',
70
+ hideInTable: true,
71
+ hideInSearch: false,
55
72
  }`);
56
73
  }
57
74
  const permCreate = permissions.create ? ` permission: '${permissions.create}',\n` : '';
@@ -105,14 +122,47 @@ ${permCreate} onClick: handleCreate,
105
122
  `;
106
123
  }
107
124
 
108
- function emitIndex(names, layout, types, fns, pagination) {
109
- const pageImport = layout.hasPageContainer
125
+ function valueTypeBlock(field) {
126
+ const valueType = columnValueType(field);
127
+ return valueType ? `\n valueType: '${valueType}',` : '';
128
+ }
129
+
130
+ function enumBlock(field) {
131
+ if (field.enum?.length) {
132
+ const entries = field.enum
133
+ .map((value) => ` ${JSON.stringify(String(value))}: { text: ${JSON.stringify(String(value))} }`)
134
+ .join(',\n');
135
+ return `\n valueEnum: {\n${entries},\n },`;
136
+ }
137
+ if (field.type === 'boolean') {
138
+ return `\n valueEnum: { true: { text: '是' }, false: { text: '否' } },`;
139
+ }
140
+ return '';
141
+ }
142
+
143
+ function emitContainerIndex() {
144
+ return `import Container from '@/components/Page/Container';
145
+
146
+ export default () => {
147
+ return <Container />;
148
+ };
149
+ `;
150
+ }
151
+
152
+ function emitIndex(names, layout, types, fns, pagination, variant = 'page') {
153
+ const isList = variant === 'list';
154
+ const usePageContainer = layout.hasPageContainer && !layout.hasContainerConfig && !isList;
155
+ const pageImport = usePageContainer
110
156
  ? `import PageContainer from '@/components/PageContainer';\n`
111
157
  : '';
112
- const open = layout.hasPageContainer
158
+ const overlayDir = isList ? './' : './components/';
159
+ const constImport = isList ? '../const' : './const';
160
+ const stylesImport = isList ? '../index.less' : './index.less';
161
+ const exportName = isList ? 'List' : names.leaf;
162
+ const open = usePageContainer
113
163
  ? ` <PageContainer header={{ title: undefined }}>`
114
164
  : ' <>';
115
- const close = layout.hasPageContainer ? ` </PageContainer>` : ' </>';
165
+ const close = usePageContainer ? ` </PageContainer>` : ' </>';
116
166
  const listAccess = pagination === 'results-count'
117
167
  ? 'list: data?.results || [],\n total: data?.count || 0,'
118
168
  : 'list: data?.list || [],\n total: data?.total || 0,';
@@ -130,14 +180,14 @@ import { message } from 'antd';
130
180
  import { useCallback, useRef } from 'react';
131
181
  import ${names.confirm}, {
132
182
  type ${names.confirm}Ref,
133
- } from './components/${names.confirm}';
183
+ } from '${overlayDir}${names.confirm}';
134
184
  import ${names.drawer}, {
135
185
  type ${names.drawer}Ref,
136
- } from './components/${names.drawer}';
137
- import { columns, getActionColumn, getToolBarRender } from './const';
138
- import styles from './index.less';
186
+ } from '${overlayDir}${names.drawer}';
187
+ import { columns, getActionColumn, getToolBarRender } from '${constImport}';
188
+ import styles from '${stylesImport}';
139
189
 
140
- export default function ${names.leaf}() {
190
+ export default function ${exportName}() {
141
191
  const tableRef = useRef<MFTableRef>(null);
142
192
  const drawerRef = useRef<${names.drawer}Ref>(null);
143
193
  const confirmRef = useRef<${names.confirm}Ref>(null);
@@ -224,8 +274,62 @@ function emitLess(layout) {
224
274
  `;
225
275
  }
226
276
 
227
- function emitDrawer(names, types, fns) {
277
+ function emitFormItem(field) {
278
+ const component = formComponent(field);
279
+ const label = JSON.stringify(field.title);
280
+ const requiredVerb = component === 'ProFormSelect' ? '请选择' : '请输入';
281
+ const required = field.required
282
+ ? `\n rules={[{ required: true, message: ${JSON.stringify(`${requiredVerb}${field.title}`)} }]}`
283
+ : '';
284
+ if (component === 'ProFormSelect') {
285
+ const options = (field.enum || [])
286
+ .map((value) => `{ label: ${JSON.stringify(String(value))}, value: ${JSON.stringify(value)} }`)
287
+ .join(', ');
288
+ return ` <ProFormSelect
289
+ name="${field.name}"
290
+ label={${label}}
291
+ options={[${options}]}${required}
292
+ />`;
293
+ }
294
+ if (component === 'ProFormSwitch') {
295
+ return ` <ProFormSwitch
296
+ name="${field.name}"
297
+ label={${label}}
298
+ />`;
299
+ }
300
+ if (component === 'ProFormDigit') {
301
+ return ` <ProFormDigit
302
+ name="${field.name}"
303
+ label={${label}}
304
+ placeholder={${JSON.stringify(`请输入${field.title}`)}}${required}
305
+ />`;
306
+ }
307
+ if (component === 'ProFormDateTimePicker' || component === 'ProFormDatePicker') {
308
+ return ` <${component}
309
+ name="${field.name}"
310
+ label={${label}}${required}
311
+ />`;
312
+ }
313
+ if (component === 'ProFormTextArea') {
314
+ return ` <ProFormTextArea
315
+ name="${field.name}"
316
+ label={${label}}
317
+ placeholder={${JSON.stringify(`请输入${field.title}`)}}${required}
318
+ />`;
319
+ }
320
+ return ` <ProFormText
321
+ name="${field.name}"
322
+ label={${label}}
323
+ placeholder={${JSON.stringify(`请输入${field.title}`)}}${required}
324
+ />`;
325
+ }
326
+
327
+ function emitDrawer(names, types, fns, formFields) {
228
328
  const itemType = types.itemType || 'Record<string, unknown>';
329
+ const fields = formFields.length
330
+ ? formFields
331
+ : [{ name: 'name', title: '名称', required: true, type: 'string' }];
332
+ const components = ['ProForm', ...new Set(fields.map(formComponent))];
229
333
  const createCall = fns.create
230
334
  ? ` await Service.${fns.create}(values);`
231
335
  : ' // TODO: call create service';
@@ -233,7 +337,7 @@ function emitDrawer(names, types, fns) {
233
337
  ? ` await Service.${fns.update}(record.id, values);`
234
338
  : ' // TODO: call update service';
235
339
  return `import Service from '@/services';
236
- import { ProForm, ProFormText } from '@ant-design/pro-components';
340
+ import { ${components.join(', ')} } from '@ant-design/pro-components';
237
341
  import { CustomDrawer, HocModal } from '@mingfuyun/star-allinone';
238
342
  import { Form, message } from 'antd';
239
343
  import { useEffect, useState } from 'react';
@@ -303,12 +407,7 @@ ${createCall}
303
407
  layout="horizontal"
304
408
  submitter={false}
305
409
  >
306
- <ProFormText
307
- name="name"
308
- label="名称"
309
- placeholder="请输入名称"
310
- rules={[{ required: true, message: '请输入名称' }]}
311
- />
410
+ ${fields.map(emitFormItem).join('\n')}
312
411
  </ProForm>
313
412
  </CustomDrawer>
314
413
  );
@@ -407,6 +506,41 @@ function patchRoute(root, names, dryRun) {
407
506
  writeFile(file, next, dryRun);
408
507
  }
409
508
 
509
+ function patchContainerConfig(root, names, dryRun) {
510
+ const file = path.join(root, 'src/components/Page/Container/config.tsx');
511
+ if (!fs.existsSync(file)) return false;
512
+ let current = fs.readFileSync(file, 'utf8');
513
+ const importPath = `@/pages/${names.featurePath}/components/List`;
514
+ const hasRoute = current.includes(`'${names.route}'`) || current.includes(`"${names.route}"`);
515
+ const hasLoader =
516
+ current.includes(`import('${importPath}')`) || current.includes(`import("${importPath}")`);
517
+ if (hasLoader && hasRoute) {
518
+ console.log(`tab container already has ${names.route}`);
519
+ return false;
520
+ }
521
+ if (!hasRoute) {
522
+ const entry = ` '${names.route}': [
523
+ {
524
+ key: 'tab-${names.leaf}',
525
+ label: ${JSON.stringify(names.title)},
526
+ loader: () => import('${importPath}'),
527
+ },
528
+ ],
529
+ `;
530
+ if (/export default \{\r?\n/.test(current)) {
531
+ current = current.replace(/export default \{\r?\n/, (match) => `${match}${entry}`);
532
+ } else if (current.includes('export default {')) {
533
+ current = current.replace('export default {', `export default {\n${entry}`);
534
+ } else {
535
+ console.log('Could not patch src/components/Page/Container/config.tsx; register the tab manually.');
536
+ return false;
537
+ }
538
+ }
539
+ writeFile(file, current, dryRun);
540
+ console.log(`${dryRun ? '[dry-run] ' : ''}registered tab ${names.route} in src/components/Page/Container/config.tsx`);
541
+ return true;
542
+ }
543
+
410
544
  function generatePage(cwd, options) {
411
545
  const operations = [
412
546
  ['list', options.listOperation],
@@ -429,6 +563,9 @@ function generatePage(cwd, options) {
429
563
  path.join(pageDir, 'components', `${names.drawer}.tsx`),
430
564
  path.join(pageDir, 'components', `${names.confirm}.tsx`),
431
565
  ];
566
+ if (layout.hasContainerConfig) {
567
+ files.push(path.join(pageDir, 'components', 'List.tsx'));
568
+ }
432
569
  const exists = files.filter((file) => fs.existsSync(file));
433
570
  if (exists.length) {
434
571
  throw new Error(`Refusing to overwrite existing page files:\n${exists.map((file) => path.relative(project.root, file)).join('\n')}`);
@@ -438,6 +575,7 @@ function generatePage(cwd, options) {
438
575
  let types = { itemType: `${toPascal(names.leaf)}_API.${names.leaf}Item`, queryType: undefined };
439
576
  let pagination = 'list-total';
440
577
  let itemSource = '';
578
+ const operationByKey = {};
441
579
 
442
580
  for (const [key, selector] of operations) {
443
581
  const result = generateService(cwd, {
@@ -446,6 +584,7 @@ function generatePage(cwd, options) {
446
584
  mockFile: false,
447
585
  });
448
586
  fns[key] = result.fn;
587
+ operationByKey[key] = result.operation;
449
588
  if (key === 'list') {
450
589
  types = {
451
590
  itemType: result.itemType || types.itemType,
@@ -459,25 +598,37 @@ function generatePage(cwd, options) {
459
598
  }
460
599
  }
461
600
 
462
- const fields = columnFields(itemSource);
601
+ const spec = project.spec;
602
+ let fields = schemaFields(spec, operationByKey.list?.itemSchema, { limit: 8 });
603
+ if (!fields.length) fields = fallbackFields(itemSource);
604
+ const formFields = schemaFields(
605
+ spec,
606
+ operationByKey.create?.bodySchema || operationByKey.update?.bodySchema || operationByKey.list?.itemSchema,
607
+ { skip: SKIP_FORM, forForm: true },
608
+ );
463
609
  const permissions = {
464
610
  create: options.permissionCreate,
465
611
  edit: options.permissionEdit,
466
612
  delete: options.permissionDelete,
467
613
  };
468
614
 
469
- writeFile(files[0], emitIndex(names, layout, types, fns, pagination), project.dryRun);
615
+ const queryNames = querySearchNames(operationByKey.list?.queryParams);
470
616
  writeFile(files[1], emitLess(layout), project.dryRun);
471
- writeFile(files[2], emitColumns(names, types.itemType, fields, permissions), project.dryRun);
472
- writeFile(files[3], emitDrawer(names, types, fns), project.dryRun);
617
+ writeFile(files[2], emitColumns(names, types.itemType, fields, permissions, queryNames), project.dryRun);
618
+ writeFile(files[3], emitDrawer(names, types, fns, formFields), project.dryRun);
473
619
  writeFile(files[4], emitConfirm(names), project.dryRun);
474
- patchRoute(project.root, names, project.dryRun);
475
-
476
620
  if (layout.hasContainerConfig) {
477
- console.log(`starscope-style tab container detected. Register ${names.featurePath} in src/components/Page/Container/config.tsx if this page should appear as a tab.`);
621
+ writeFile(files[0], emitContainerIndex(), project.dryRun);
622
+ writeFile(files[5], emitIndex(names, layout, types, fns, pagination, 'list'), project.dryRun);
623
+ patchRoute(project.root, names, project.dryRun);
624
+ patchContainerConfig(project.root, names, project.dryRun);
625
+ } else {
626
+ writeFile(files[0], emitIndex(names, layout, types, fns, pagination), project.dryRun);
627
+ patchRoute(project.root, names, project.dryRun);
478
628
  }
629
+
479
630
  console.log(`${project.dryRun ? '[dry-run] ' : ''}generated page ${names.route} -> src/pages/${names.featurePath}`);
480
631
  return names;
481
632
  }
482
633
 
483
- module.exports = { generatePage, featureNames };
634
+ module.exports = { generatePage, featureNames, patchContainerConfig };
package/skills/README.md CHANGED
@@ -46,11 +46,13 @@ Cursor / Codex 的常驻说明只放仓库根 `AGENTS.md`(模板见 `examples/
46
46
  ├── design-to-page/ # 页面;标准列表内部跑 gen page
47
47
  ├── review-and-submit/ # 审查并提交;步骤在 references/
48
48
  ├── publish-test/ # 测发
49
- ├── project-conventions/ # architecture / review-checklist / quality-checklist
49
+ ├── project-conventions/ # architecture / component-contract / mfprotable / review-checklist / quality-checklist
50
50
  └── i-have-adhd/ # 用户输出格式
51
51
  ```
52
52
 
53
- 公共入口写死两边已对齐的硬规则,例如:页面走 `MFRequest`、DTO 放 namespace typings、`MFProTable` + `ProForm`、弹层用 `BaseModal` / `CustomDrawer` / `HocModal`、`await` 必须 try/catch、禁止无说明的 `any`。
53
+ 公共入口写死两边已对齐的硬规则,例如:页面走 `MFRequest`、DTO 放 namespace typings、表格用业务仓 `@/components/CommonTable/MFProTable`(不要从包里找 `MFTablePro`)、`ProForm`、弹层从 `@mingfuyun/star-allinone` 导入 `BaseModal` / `CustomDrawer` / `HocModal`、`await` 必须 try/catch、禁止无说明的 `any`、React 18。
54
+
55
+ 接入或升级后跑 `pnpm exec star-allinone doctor`。
54
56
 
55
57
  业务项目的 `.agents/project/` 只写差异:路径、权限体系、OpenAPI 位置、领域特例。项目文件可以补充,但不能削弱公共硬规则。
56
58
 
@@ -70,6 +72,7 @@ npm 包中的 skills/shared
70
72
 
71
73
  ```bash
72
74
  pnpm exec star-allinone skills sync
75
+ pnpm exec star-allinone doctor
73
76
  ```
74
77
 
75
78
  同步会整体替换 `shared-*` 目录,并删除包中已不存在的过期 `shared-*`,但不会改动 `custom-*`。
@@ -132,7 +135,7 @@ git commit -m "chore(skill): 更新公共 skill"
132
135
  | `shared-design-to-page` | SKILL + `page-conventions.md`、`overlays.md`、`list-page-template.md` | 页面分层、标准列表 CLI、弹层合同 |
133
136
  | `shared-review-and-submit` | SKILL + `impact-analysis.md`、`verify.md`、`ai-review.md`、`commit.md` | 影响分析 → 验证 → 确认后提交 |
134
137
  | `shared-publish-test` | SKILL + `scripts/publish-current-branch.sh` | 云效测发;只读 `publishPipelineId` |
135
- | (非入口)`shared-project-conventions` | `architecture.md`、`review-checklist.md`、`quality-checklist.md` | 架构与质量清单 |
138
+ | (非入口)`shared-project-conventions` | `architecture.md`、`mfprotable.md`、`component-contract.md`、`review-checklist.md`、`quality-checklist.md` | 架构、导入合同、质量清单 |
136
139
  | (非入口)`shared-i-have-adhd` | `i-have-adhd.md` | 用户输出格式 |
137
140
 
138
141
  以下内容**不**写进公共 skill 源码:
@@ -141,6 +144,16 @@ git commit -m "chore(skill): 更新公共 skill"
141
144
  - OpenAPI 快照:业务仓库数据文件
142
145
  - 真正的业务特例:权限常量名、IP 校验、设备/节点领域规则
143
146
 
147
+ ## 云效门禁
148
+
149
+ 业务仓通常没有 pipeline YAML。在云效构建控制台显式增加:
150
+
151
+ ```bash
152
+ pnpm typecheck && pnpm lint
153
+ ```
154
+
155
+ 再跑现有 `pnpm build`。`pnpm test` 若实际是 `max dev`(测网开发服务),改名为 `start:test` / `dev:test`,避免 Agent 当成单测。
156
+
144
157
  ## 最终推荐流程
145
158
 
146
159
  ```text
@@ -48,8 +48,8 @@ pnpm exec star-allinone gen page \
48
48
  Add `--permission-create`, `--permission-edit`, `--permission-delete` when codes are known.
49
49
 
50
50
  2. If only services are missing, run `pnpm exec star-allinone gen service --operation "METHOD /path/"` first. Follow `.agents/skills/shared-api-to-service/SKILL.md`.
51
- 3. Fill generated placeholders using [references/list-page-template.md](references/list-page-template.md): columns, form fields, rowKey, permission constants, and pagination adapter (`results/count` vs `list/total`).
52
- 4. If `src/components/Page/Container/config.tsx` exists, register the new page as a tab after generating the standalone files. Do not invent a second page shell.
51
+ 3. The CLI fills columns and drawer fields from the OpenAPI schema (required, enum → `ProFormSelect`, dates/numbers). Review labels and permission codes; do not recreate the file tree. See [references/list-page-template.md](references/list-page-template.md).
52
+ 4. If `src/components/Page/Container/config.tsx` exists, `gen page` writes a Container `index.tsx`, puts the table in `components/List.tsx`, and registers a Tab with `loader: () => import(...)`. Do not invent a second page shell.
53
53
  5. Do not overwrite existing page files. Tell the user the CLI refused, then edit in place.
54
54
 
55
55
  ## Design path
@@ -67,7 +67,7 @@ For non-standard layouts:
67
67
  ## Hard rules
68
68
 
69
69
  - React function components, TypeScript, Ant Design 5, Pro Components, Less.
70
- - `ProForm` for business forms. `MFProTable` for management lists.
70
+ - `ProForm` for business forms. `MFProTable` from `@/components/CommonTable/MFProTable` (not from the npm package).
71
71
  - Overlays from `@mingfuyun/star-allinone`: `CustomDrawer`, `BaseModal`, `HocModal`.
72
72
  - Pages consume `import Service from '@/services'` only.
73
73
  - Keep columns and toolbar builders in `const.tsx`. Empty cells render as `-`.
@@ -13,9 +13,11 @@ src/pages/<Feature>/
13
13
  └── <Feature>ConfirmModal.tsx
14
14
  ```
15
15
 
16
- ## index.tsx
16
+ When `src/components/Page/Container/config.tsx` exists, `index.tsx` is a Container shell and the table lives in `components/List.tsx`. The CLI also registers a tab for the new route.
17
17
 
18
- - Wrap with `PageContainer` when `src/components/PageContainer` exists.
18
+ ## index.tsx / List.tsx
19
+
20
+ - Wrap with `PageContainer` when `src/components/PageContainer` exists and the page is not a Container tab.
19
21
  - `fetchData` is a `useCallback` that calls `Service.<listFn>`, maps pagination, and returns `{ code, data: { list, total } }`.
20
22
  - Open the drawer with `drawerRef.current?.show({ record })`.
21
23
  - Destructive actions go through the confirm modal ref, then `tableRef.current?.reload()`.
@@ -23,6 +25,7 @@ src/pages/<Feature>/
23
25
  ## const.tsx
24
26
 
25
27
  - Export `columns`, `getActionColumn`, `getToolBarRender`.
28
+ - Columns come from the list item schema: types, enums, and the first string fields as search.
26
29
  - Put permission strings on actions and toolbar items when the project uses them.
27
30
  - Render empty cells as `'-'`.
28
31
 
@@ -30,6 +33,7 @@ src/pages/<Feature>/
30
33
 
31
34
  - `CustomDrawer` + `HocModal`.
32
35
  - `ProForm` with `submitter={false}` and `layout="horizontal"`.
36
+ - Fields come from the create/update body schema: required rules, `enum` → `ProFormSelect`, numbers → `ProFormDigit`, dates → date pickers.
33
37
  - `validateFields()` in `try`. Create vs update from `record`.
34
38
  - Reset fields when `visible` becomes true.
35
39
 
@@ -41,4 +45,10 @@ src/pages/<Feature>/
41
45
 
42
46
  ## Container tabs
43
47
 
44
- If the project uses `src/components/Page/Container/config.tsx`, the generated `src/pages/<Feature>/index.tsx` is still the source of truth. Add a tab entry that renders the feature component. Do not duplicate fetch logic inside Container.
48
+ If the project uses `src/components/Page/Container/config.tsx`, the CLI:
49
+
50
+ 1. Writes `index.tsx` as `import Container from '@/components/Page/Container'`.
51
+ 2. Writes the table in `components/List.tsx`.
52
+ 3. Registers `'<route>': [{ key, label, loader: () => import('.../List') }]` in `config.tsx`. Do not add a static page import or `children: <List />`.
53
+
54
+ Do not duplicate fetch logic inside Container. Do not wrap the List with a second `PageContainer`.
@@ -2,6 +2,12 @@
2
2
 
3
3
  Use this reference for every modal, confirmation dialog, and drawer. Import `@mingfuyun/star-allinone/dist/styles.css` once from the application entry, not from feature components.
4
4
 
5
+ Overlays belong to the npm package. Do not add `src/components/CustomDrawer`, `src/components/BaseModal`, or `src/components/HocModal`.
6
+
7
+ ```tsx
8
+ import { CustomDrawer, BaseModal, HocModal } from '@mingfuyun/star-allinone';
9
+ ```
10
+
5
11
  ## Required components
6
12
 
7
13
  - `CustomDrawer` for create, edit, detail, or configuration drawers.
@@ -31,7 +31,8 @@ Styles live in the current page directory. Reuse shared mixins such as `@/styles
31
31
 
32
32
  ## Tables
33
33
 
34
- - Prefer `MFProTable` for management lists. Use Ant Design `Table` only for a deliberately simple static shell.
34
+ - Prefer `MFProTable` for management lists. Import it from `@/components/CommonTable/MFProTable`. See [mfprotable.md](../../project-conventions/references/mfprotable.md). Use Ant Design `Table` only for a deliberately simple static shell.
35
+ - Do not import `MFTablePro` or any table from `@mingfuyun/star-allinone`.
35
36
  - Extract `fetchData` as an `async` method. Do not inline request logic in JSX props.
36
37
  - Wrap `await` in `try-catch` and return a table-safe empty shape on failure.
37
38
  - Adapt backend pagination to the table contract. Common backend shapes are `{ results, count }` or `{ list, total }`; the table adapter is typically `{ code, data: { list, total } }`. Follow the nearest page.
@@ -8,10 +8,11 @@ Apply these rules to every non-trivial frontend change. Shared review, commit, m
8
8
  - Use React function components and TypeScript.
9
9
  - Put transport code in `src/services`. Pages must not call axios or the request helper directly.
10
10
  - Keep API DTOs in the domain `typings.d.ts`. Use named types and adapters instead of `any`.
11
- - Reuse `MFProTable`, `BaseModal`, `CustomDrawer`, `HocModal`, permission wrappers, table cells, hooks, and shared styles before adding abstractions.
11
+ - Reuse `MFProTable` from `@/components/CommonTable/MFProTable` (see [component-contract.md](./component-contract.md) and [mfprotable.md](./mfprotable.md)). It is not exported by `@mingfuyun/star-allinone`.
12
+ - Overlays (`CustomDrawer`, `BaseModal`, `HocModal`) and `ErrorBoundary` / `Cron` come from `@mingfuyun/star-allinone`. Do not keep local copies under `src/components`.
12
13
  - Use `@/` imports for application modules and keep feature-local imports relative.
13
14
  - Do not add a UI/state library or duplicate an existing component without explicit approval.
14
- - Prefer feature-level code splitting for Excel, editors, charts, drag-and-drop, and other heavy modules.
15
+ - Prefer feature-level code splitting for Excel, editors, charts, drag-and-drop, and other heavy modules. `xlsx`, `@uiw/react-codemirror` / `@codemirror` / monaco, and chart libraries must use `import()`. Do not static-import them from `src/app.ts` or generated list pages.
15
16
 
16
17
  ## Business behavior
17
18
 
@@ -43,4 +44,6 @@ Prefer `pnpm typecheck` when that script exists.
43
44
 
44
45
  Run `pnpm lint` for broad or shared changes. Run `pnpm run build` when routes, entry behavior, shared runtime, or multiple page modules change. If `.star-allinone.json` documents a TypeScript `baseUrl` or build script, use that command.
45
46
 
47
+ Yunxiao (and any CI without in-repo pipeline YAML) must run `pnpm typecheck && pnpm lint` as an explicit build step. `pnpm test` / `pnpm start:test` in business apps starts the test-env dev server; it is not a unit-test suite.
48
+
46
49
  Distinguish failures introduced by the current change from pre-existing or environment failures.
@@ -0,0 +1,38 @@
1
+ # Component contract
2
+
3
+ Use this file as the import source of truth. Do not invent package exports.
4
+
5
+ ## From `@mingfuyun/star-allinone`
6
+
7
+ The npm package ships overlays, Cron, and ErrorBoundary. It does **not** export a table.
8
+
9
+ ```ts
10
+ import {
11
+ BaseModal,
12
+ Cron,
13
+ CustomDrawer,
14
+ ErrorBoundary,
15
+ HocModal,
16
+ } from '@mingfuyun/star-allinone';
17
+ import '@mingfuyun/star-allinone/dist/styles.css';
18
+ ```
19
+
20
+ Do not write any of the following:
21
+
22
+ - `import { MFTablePro } from '@mingfuyun/star-allinone'`
23
+ - `import { MFProTable } from '@mingfuyun/star-allinone'`
24
+ - `@mingfuyun/star-allinone/components/BaseModal` (use the main export)
25
+
26
+ ## From the business app
27
+
28
+ Management lists use the local wrapper. Props and usage are in [mfprotable.md](./mfprotable.md).
29
+
30
+ ```ts
31
+ import MFProTable from '@/components/CommonTable/MFProTable';
32
+ ```
33
+
34
+ Pages still consume APIs with `import Service from '@/services'`. Overlay shells come from the package above, not from `@/components/CustomDrawer` or `@/components/BaseModal`.
35
+
36
+ ## Runtime
37
+
38
+ Peer React is **18.3.x**. Do not assume React 19 APIs. Ant Design 5 and Pro Components live in the business app.
@@ -0,0 +1,59 @@
1
+ # MFProTable contract
2
+
3
+ `MFProTable` lives in the **business app**, not in `@mingfuyun/star-allinone`. Do not invent `MFTablePro` and do not import a table from the npm package.
4
+
5
+ ```tsx
6
+ import MFProTable from '@/components/CommonTable/MFProTable';
7
+ import type { MFTableRef } from '@/components/CommonTable/type';
8
+ ```
9
+
10
+ The capability package will not absorb this component until peer/externals/Umi versions are stable across apps.
11
+
12
+ ## Required usage
13
+
14
+ ```tsx
15
+ <MFProTable<Item, Query>
16
+ ref={tableRef}
17
+ actionColumn={{
18
+ actions: getActionColumn({ handleEdit, handleDelete }),
19
+ }}
20
+ bordered
21
+ columns={columns()}
22
+ fetchData={fetchData}
23
+ queryParams={{}}
24
+ rowKey="id"
25
+ scroll={{ x: 'max-content' }}
26
+ topTitle={false}
27
+ toolBarRender={getToolBarRender({ handleCreate })}
28
+ />
29
+ ```
30
+
31
+ - `fetchData` is an `async` callback. On failure return `{ code: 0, data: { list: [], total: 0 } }`.
32
+ - Adapt backend `{ results, count }` or `{ list, total }` to `{ code, data: { list, total } }`.
33
+ - Put columns, action builders, and toolbar items in `const.tsx`.
34
+ - Empty display values render as `'-'`.
35
+ - Open overlays through refs. Do not store drawer visibility on the page when `HocModal` owns it.
36
+
37
+ ## Common props
38
+
39
+ These names match `src/components/CommonTable/type.ts` (`ITableProps` / `MFTableRef`):
40
+
41
+ | Prop | Role |
42
+ | --- | --- |
43
+ | `columns` | `ProColumns<T>[]` from `const.tsx` |
44
+ | `fetchData` | `(params) => Promise<{ code, data: { list, total } }>` |
45
+ | `queryParams` | Extra query merged into requests |
46
+ | `rowKey` | Stable id field, usually `id` |
47
+ | `actionColumn.actions` | Row actions; `permission` when the project uses it |
48
+ | `toolBarRender` | Toolbar buttons (`TableSearchType[]`) |
49
+ | `scroll` | Wide tables: `{ x: 'max-content' }` |
50
+ | `columnsStateKey` | Persist column state via the project hook |
51
+ | `ref.reload` / `ref.reloadAndReset` | After mutations |
52
+
53
+ Permission filtering is implemented **in the app** (`useAuthFilter`, `withAuth`, `TableActions`). The package overlays do not wrap table permissions.
54
+
55
+ ## Do not
56
+
57
+ - `import { MFTablePro } from '@mingfuyun/star-allinone'`
58
+ - `import { MFProTable } from '@mingfuyun/star-allinone'`
59
+ - Copy `MFProTable` into a second path and import both.
@@ -17,3 +17,17 @@ Check unsafe HTML, URL construction, upload/download names, sensitive logs/stora
17
17
  ## Testing
18
18
 
19
19
  Request tests for concrete regression boundaries such as transformations, races, conditional validation, reusable hooks, complex transitions, or likely recurring bugs. Run focused Prettier, TypeScript, lint, `git diff --check`, and a production build in proportion to risk.
20
+
21
+ ## CI / Yunxiao
22
+
23
+ These apps do not keep pipeline YAML in the repo. In the Yunxiao build step run:
24
+
25
+ ```bash
26
+ pnpm typecheck && pnpm lint
27
+ ```
28
+
29
+ `pnpm test` is not a unit-test command in the business apps (use `start:test` / `dev:test` for the test-env dev server). `@mingfuyun/star-allinone` `npm test` only covers codegen fixtures.
30
+
31
+ ## Performance
32
+
33
+ Flag new top-level imports of `xlsx`, `@uiw/react-codemirror` / CodeMirror, chart libraries, and similar heavy modules. They must be `import()` inside the feature that needs them. Generated pages must not pull them into `src/app.ts`.
@@ -19,8 +19,8 @@ This checklist is complete for shared review and commit. If `.agents/project/rev
19
19
 
20
20
  ## Pages and forms
21
21
 
22
- - Management lists prefer `MFProTable`. Column and toolbar configuration belong outside the render function.
23
- - Business forms use `ProForm`. Drawers use `CustomDrawer`. Dialogs use `BaseModal` / `HocModal`.
22
+ - Management lists prefer `MFProTable` from `@/components/CommonTable/MFProTable`. Column and toolbar configuration belong outside the render function.
23
+ - Business forms use `ProForm`. Drawers use `CustomDrawer`. Dialogs use `BaseModal` / `HocModal` from `@mingfuyun/star-allinone`.
24
24
  - Successful mutations refresh the parent list. Closing an overlay clears form, loading, selected record, and other async state.
25
25
  - Every `await` has error handling. Loading restores in `finally`.
26
26
  - Check loading, empty, error, disabled, long content, and narrow-screen layout.
@@ -41,6 +41,16 @@ Read `.star-allinone.json` before running. `publishPipelineId` is required:
41
41
 
42
42
  Never ask the user to repeat the pipeline ID. Never copy another project's pipeline ID.
43
43
 
44
+ ## Yunxiao build gates
45
+
46
+ The pipeline lives in the Yunxiao console (there is no in-repo YAML). The build step must run:
47
+
48
+ ```bash
49
+ pnpm typecheck && pnpm lint
50
+ ```
51
+
52
+ before or as part of `max build`. Do not treat `pnpm test` as unit tests; in business apps that script was the test-env dev server and is now `start:test` / `dev:test`. `ANALYZE=1` is a manual pre-release chunk check, not a pipeline substitute.
53
+
44
54
  ## Preconditions
45
55
 
46
56
  - Current HEAD is a named branch, not detached.