@mingfuyun/star-allinone 0.2.0 → 0.3.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 +21 -9
- package/examples/AGENTS.md +29 -0
- package/examples/projects/starlink-frontend.star-allinone.json +7 -0
- package/examples/projects/starscope-frontend.star-allinone.json +7 -0
- package/examples/skills/custom-device-service/SKILL.md +18 -0
- package/package.json +5 -3
- package/scripts/cli.js +44 -11
- package/scripts/gen/fixtures/run.js +104 -0
- package/scripts/gen/fixtures/widgets.openapi.json +194 -0
- package/scripts/gen/names.js +117 -0
- package/scripts/gen/page.js +483 -0
- package/scripts/gen/project.js +154 -0
- package/scripts/gen/service.js +264 -0
- package/scripts/gen/spec.js +220 -0
- package/scripts/gen/ts.js +121 -0
- package/skills/README.md +31 -3
- package/skills/shared/ai-review/SKILL.md +4 -1
- package/skills/shared/api-mock/SKILL.md +4 -1
- package/skills/shared/api-to-service/SKILL.md +16 -8
- package/skills/shared/commit-current-changes/SKILL.md +4 -1
- package/skills/shared/design-to-page/SKILL.md +5 -1
- package/skills/shared/i-have-adhd/SKILL.md +1 -1
- package/skills/shared/impact-analysis/SKILL.md +4 -1
- package/skills/shared/page-recipe/SKILL.md +72 -0
- package/skills/shared/page-recipe/references/list-page-template.md +44 -0
- package/skills/shared/project-conventions/SKILL.md +4 -1
- package/skills/shared/project-conventions/references/architecture.md +3 -0
- package/skills/shared/publish-test/SKILL.md +3 -1
- package/skills/shared/review-and-submit/SKILL.md +3 -1
- package/skills/shared/verify/SKILL.md +5 -2
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { toPascal, parseOperationSelector } = require('./names');
|
|
4
|
+
const { generateService } = require('./service');
|
|
5
|
+
const { loadProject, writeFile } = require('./project');
|
|
6
|
+
|
|
7
|
+
function detectLayout(root) {
|
|
8
|
+
const hasPageContainer = fs.existsSync(path.join(root, 'src/components/PageContainer/index.tsx'));
|
|
9
|
+
const hasProTableLess = fs.existsSync(path.join(root, 'src/styles/proTable.less'));
|
|
10
|
+
const hasContainerConfig = fs.existsSync(path.join(root, 'src/components/Page/Container/config.tsx'));
|
|
11
|
+
return { hasPageContainer, hasProTableLess, hasContainerConfig };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function featureNames(options) {
|
|
15
|
+
const feature = options.feature.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
16
|
+
if (!feature) throw new Error('Missing --feature. Example: --feature Widgets');
|
|
17
|
+
const parts = feature.split('/');
|
|
18
|
+
const leaf = toPascal(parts[parts.length - 1]);
|
|
19
|
+
const dir = [...parts.slice(0, -1), leaf].join('/');
|
|
20
|
+
return {
|
|
21
|
+
featurePath: dir,
|
|
22
|
+
leaf,
|
|
23
|
+
component: `./${dir}`,
|
|
24
|
+
drawer: `${leaf}Drawer`,
|
|
25
|
+
confirm: `${leaf}ConfirmModal`,
|
|
26
|
+
title: options.title || leaf,
|
|
27
|
+
route: options.route || `/${leaf.replace(/[A-Z]/g, (ch, i) => (i ? '-' : '') + ch.toLowerCase())}`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
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);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function emitColumns(names, itemType, fields, permissions) {
|
|
38
|
+
const columns = fields.map((field, index) => {
|
|
39
|
+
const search = index < 2;
|
|
40
|
+
return ` {
|
|
41
|
+
title: '${field}',
|
|
42
|
+
dataIndex: '${field}',
|
|
43
|
+
key: '${field}',
|
|
44
|
+
hideInSearch: ${search ? 'false' : 'true'},
|
|
45
|
+
ellipsis: true,
|
|
46
|
+
render: (_, record) => record.${field} ?? '-',
|
|
47
|
+
}`;
|
|
48
|
+
});
|
|
49
|
+
if (!columns.length) {
|
|
50
|
+
columns.push(` {
|
|
51
|
+
title: 'ID',
|
|
52
|
+
dataIndex: 'id',
|
|
53
|
+
key: 'id',
|
|
54
|
+
hideInSearch: true,
|
|
55
|
+
}`);
|
|
56
|
+
}
|
|
57
|
+
const permCreate = permissions.create ? ` permission: '${permissions.create}',\n` : '';
|
|
58
|
+
const permEdit = permissions.edit ? ` permission: '${permissions.edit}',\n` : '';
|
|
59
|
+
const permDelete = permissions.delete ? ` permission: '${permissions.delete}',\n` : '';
|
|
60
|
+
return `import type {
|
|
61
|
+
ITableProps,
|
|
62
|
+
TableSearchType,
|
|
63
|
+
} from '@/components/CommonTable/type';
|
|
64
|
+
import type { ProColumns } from '@ant-design/pro-components';
|
|
65
|
+
|
|
66
|
+
type Handlers = {
|
|
67
|
+
handleEdit: (record: ${itemType}) => void;
|
|
68
|
+
handleDelete: (record: ${itemType}) => void;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type ToolbarHandlers = { handleCreate: () => void };
|
|
72
|
+
|
|
73
|
+
export const columns = (): ProColumns<${itemType}>[] => [
|
|
74
|
+
${columns.join(',\n')},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export const getActionColumn = ({
|
|
78
|
+
handleEdit,
|
|
79
|
+
handleDelete,
|
|
80
|
+
}: Handlers): NonNullable<
|
|
81
|
+
ITableProps<${itemType}>['actionColumn']
|
|
82
|
+
>['actions'] => [
|
|
83
|
+
{
|
|
84
|
+
key: 'edit',
|
|
85
|
+
${permEdit} getText: () => '编辑',
|
|
86
|
+
onClick: handleEdit,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: 'delete',
|
|
90
|
+
${permDelete} getText: () => '删除',
|
|
91
|
+
onClick: handleDelete,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
export const getToolBarRender = ({
|
|
96
|
+
handleCreate,
|
|
97
|
+
}: ToolbarHandlers): TableSearchType[] => [
|
|
98
|
+
{
|
|
99
|
+
key: 'create',
|
|
100
|
+
text: '新增',
|
|
101
|
+
type: 'primary',
|
|
102
|
+
${permCreate} onClick: handleCreate,
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function emitIndex(names, layout, types, fns, pagination) {
|
|
109
|
+
const pageImport = layout.hasPageContainer
|
|
110
|
+
? `import PageContainer from '@/components/PageContainer';\n`
|
|
111
|
+
: '';
|
|
112
|
+
const open = layout.hasPageContainer
|
|
113
|
+
? ` <PageContainer header={{ title: undefined }}>`
|
|
114
|
+
: ' <>';
|
|
115
|
+
const close = layout.hasPageContainer ? ` </PageContainer>` : ' </>';
|
|
116
|
+
const listAccess = pagination === 'results-count'
|
|
117
|
+
? 'list: data?.results || [],\n total: data?.count || 0,'
|
|
118
|
+
: 'list: data?.list || [],\n total: data?.total || 0,';
|
|
119
|
+
const queryType = types.queryType || 'Record<string, unknown>';
|
|
120
|
+
const itemType = types.itemType || 'Record<string, unknown>';
|
|
121
|
+
const listFn = fns.list || `list${names.leaf}`;
|
|
122
|
+
const deleteCall = fns.remove
|
|
123
|
+
? ` await Service.${fns.remove}(record.id);`
|
|
124
|
+
: ` // TODO: call delete service`;
|
|
125
|
+
const rowKey = 'id';
|
|
126
|
+
return `import MFProTable from '@/components/CommonTable/MFProTable';
|
|
127
|
+
import type { MFTableRef } from '@/components/CommonTable/type';
|
|
128
|
+
${pageImport}import Service from '@/services';
|
|
129
|
+
import { message } from 'antd';
|
|
130
|
+
import { useCallback, useRef } from 'react';
|
|
131
|
+
import ${names.confirm}, {
|
|
132
|
+
type ${names.confirm}Ref,
|
|
133
|
+
} from './components/${names.confirm}';
|
|
134
|
+
import ${names.drawer}, {
|
|
135
|
+
type ${names.drawer}Ref,
|
|
136
|
+
} from './components/${names.drawer}';
|
|
137
|
+
import { columns, getActionColumn, getToolBarRender } from './const';
|
|
138
|
+
import styles from './index.less';
|
|
139
|
+
|
|
140
|
+
export default function ${names.leaf}() {
|
|
141
|
+
const tableRef = useRef<MFTableRef>(null);
|
|
142
|
+
const drawerRef = useRef<${names.drawer}Ref>(null);
|
|
143
|
+
const confirmRef = useRef<${names.confirm}Ref>(null);
|
|
144
|
+
|
|
145
|
+
const fetchData = useCallback(async (params: ${queryType}) => {
|
|
146
|
+
try {
|
|
147
|
+
const { data } = await Service.${listFn}(params);
|
|
148
|
+
return {
|
|
149
|
+
code: 0,
|
|
150
|
+
data: {
|
|
151
|
+
${listAccess}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('获取${names.title}失败:', error);
|
|
156
|
+
return {
|
|
157
|
+
code: 0,
|
|
158
|
+
data: {
|
|
159
|
+
list: [],
|
|
160
|
+
total: 0,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const handleCreate = () => {
|
|
167
|
+
drawerRef.current?.show();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const handleEdit = (record: ${itemType}) => {
|
|
171
|
+
drawerRef.current?.show({ record });
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const handleDelete = (record: ${itemType}) => {
|
|
175
|
+
confirmRef.current?.show({
|
|
176
|
+
title: \`确定删除该记录吗?\`,
|
|
177
|
+
onConfirm: async () => {
|
|
178
|
+
${deleteCall}
|
|
179
|
+
message.success('删除成功');
|
|
180
|
+
tableRef.current?.reload();
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
${open}
|
|
187
|
+
<div className={styles.page}>
|
|
188
|
+
<MFProTable<${itemType}, ${queryType}>
|
|
189
|
+
ref={tableRef}
|
|
190
|
+
actionColumn={{
|
|
191
|
+
actions: getActionColumn({ handleEdit, handleDelete }),
|
|
192
|
+
}}
|
|
193
|
+
bordered
|
|
194
|
+
columns={columns()}
|
|
195
|
+
fetchData={fetchData}
|
|
196
|
+
queryParams={{}}
|
|
197
|
+
rowKey="${rowKey}"
|
|
198
|
+
scroll={{ x: 'max-content' }}
|
|
199
|
+
topTitle={false}
|
|
200
|
+
toolBarRender={getToolBarRender({ handleCreate })}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
<${names.drawer} ref={drawerRef} reload={() => tableRef.current?.reload()} />
|
|
204
|
+
<${names.confirm} ref={confirmRef} />
|
|
205
|
+
${close}
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function emitLess(layout) {
|
|
212
|
+
if (layout.hasProTableLess) {
|
|
213
|
+
return `@import '@/styles/proTable.less';
|
|
214
|
+
|
|
215
|
+
.page {
|
|
216
|
+
min-width: 0;
|
|
217
|
+
.pro-table-page(false, false);
|
|
218
|
+
}
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
return `.page {
|
|
222
|
+
min-width: 0;
|
|
223
|
+
}
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function emitDrawer(names, types, fns) {
|
|
228
|
+
const itemType = types.itemType || 'Record<string, unknown>';
|
|
229
|
+
const createCall = fns.create
|
|
230
|
+
? ` await Service.${fns.create}(values);`
|
|
231
|
+
: ' // TODO: call create service';
|
|
232
|
+
const updateCall = fns.update
|
|
233
|
+
? ` await Service.${fns.update}(record.id, values);`
|
|
234
|
+
: ' // TODO: call update service';
|
|
235
|
+
return `import Service from '@/services';
|
|
236
|
+
import { ProForm, ProFormText } from '@ant-design/pro-components';
|
|
237
|
+
import { CustomDrawer, HocModal } from '@mingfuyun/star-allinone';
|
|
238
|
+
import { Form, message } from 'antd';
|
|
239
|
+
import { useEffect, useState } from 'react';
|
|
240
|
+
|
|
241
|
+
type ${names.drawer}Args = {
|
|
242
|
+
record?: ${itemType};
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
type Props = ${names.drawer}Args & {
|
|
246
|
+
visible: boolean;
|
|
247
|
+
handleCancel: () => void;
|
|
248
|
+
reload: () => void;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export interface ${names.drawer}Ref {
|
|
252
|
+
show: (args?: ${names.drawer}Args) => void;
|
|
253
|
+
hide: () => void;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const ${names.drawer}: React.FC<Props> = ({
|
|
257
|
+
visible,
|
|
258
|
+
handleCancel,
|
|
259
|
+
record,
|
|
260
|
+
reload,
|
|
261
|
+
}) => {
|
|
262
|
+
const [form] = Form.useForm();
|
|
263
|
+
const [loading, setLoading] = useState(false);
|
|
264
|
+
const editing = Boolean(record);
|
|
265
|
+
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (!visible) return;
|
|
268
|
+
form.resetFields();
|
|
269
|
+
if (record) form.setFieldsValue(record);
|
|
270
|
+
}, [form, record, visible]);
|
|
271
|
+
|
|
272
|
+
const handleConfirm = async () => {
|
|
273
|
+
try {
|
|
274
|
+
const values = await form.validateFields();
|
|
275
|
+
setLoading(true);
|
|
276
|
+
if (editing && record) {
|
|
277
|
+
${updateCall}
|
|
278
|
+
} else {
|
|
279
|
+
${createCall}
|
|
280
|
+
}
|
|
281
|
+
message.success(editing ? '编辑成功' : '新增成功');
|
|
282
|
+
reload();
|
|
283
|
+
handleCancel();
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error('提交${names.title}失败:', error);
|
|
286
|
+
} finally {
|
|
287
|
+
setLoading(false);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<CustomDrawer
|
|
293
|
+
loading={loading}
|
|
294
|
+
open={visible}
|
|
295
|
+
title={editing ? '编辑${names.title}' : '新增${names.title}'}
|
|
296
|
+
width={480}
|
|
297
|
+
onClose={handleCancel}
|
|
298
|
+
onConfirm={handleConfirm}
|
|
299
|
+
>
|
|
300
|
+
<ProForm
|
|
301
|
+
form={form}
|
|
302
|
+
labelCol={{ flex: '88px' }}
|
|
303
|
+
layout="horizontal"
|
|
304
|
+
submitter={false}
|
|
305
|
+
>
|
|
306
|
+
<ProFormText
|
|
307
|
+
name="name"
|
|
308
|
+
label="名称"
|
|
309
|
+
placeholder="请输入名称"
|
|
310
|
+
rules={[{ required: true, message: '请输入名称' }]}
|
|
311
|
+
/>
|
|
312
|
+
</ProForm>
|
|
313
|
+
</CustomDrawer>
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export default HocModal(${names.drawer});
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function emitConfirm(names) {
|
|
322
|
+
return `import { BaseModal, HocModal } from '@mingfuyun/star-allinone';
|
|
323
|
+
import { Typography } from 'antd';
|
|
324
|
+
import { useState } from 'react';
|
|
325
|
+
|
|
326
|
+
export type ${names.confirm}Args = {
|
|
327
|
+
title: string;
|
|
328
|
+
content?: React.ReactNode;
|
|
329
|
+
onConfirm: () => Promise<void>;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
type Props = ${names.confirm}Args & {
|
|
333
|
+
visible: boolean;
|
|
334
|
+
handleCancel: () => void;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export interface ${names.confirm}Ref {
|
|
338
|
+
show: (args: ${names.confirm}Args) => void;
|
|
339
|
+
hide: () => void;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const ${names.confirm}: React.FC<Props> = ({
|
|
343
|
+
visible,
|
|
344
|
+
handleCancel,
|
|
345
|
+
title,
|
|
346
|
+
content,
|
|
347
|
+
onConfirm,
|
|
348
|
+
}) => {
|
|
349
|
+
const [loading, setLoading] = useState(false);
|
|
350
|
+
|
|
351
|
+
const handleOk = async () => {
|
|
352
|
+
setLoading(true);
|
|
353
|
+
try {
|
|
354
|
+
await onConfirm();
|
|
355
|
+
handleCancel();
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error('确认操作失败:', error);
|
|
358
|
+
} finally {
|
|
359
|
+
setLoading(false);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<BaseModal
|
|
365
|
+
confirmLoading={loading}
|
|
366
|
+
destroyOnClose
|
|
367
|
+
maskClosable={false}
|
|
368
|
+
title={title}
|
|
369
|
+
visible={visible}
|
|
370
|
+
width={480}
|
|
371
|
+
onCancel={handleCancel}
|
|
372
|
+
onOk={handleOk}
|
|
373
|
+
>
|
|
374
|
+
<Typography.Paragraph style={{ marginBottom: 0 }}>
|
|
375
|
+
{content || '操作提交后将立即生效,请确认是否继续。'}
|
|
376
|
+
</Typography.Paragraph>
|
|
377
|
+
</BaseModal>
|
|
378
|
+
);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
export default HocModal(${names.confirm});
|
|
382
|
+
`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function patchRoute(root, names, dryRun) {
|
|
386
|
+
const file = path.join(root, 'config/route.ts');
|
|
387
|
+
if (!fs.existsSync(file)) {
|
|
388
|
+
throw new Error('Missing config/route.ts');
|
|
389
|
+
}
|
|
390
|
+
const current = fs.readFileSync(file, 'utf8');
|
|
391
|
+
if (current.includes(`path: '${names.route}'`) || current.includes(`path: "${names.route}"`)) {
|
|
392
|
+
console.log(`route already has ${names.route}`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const snippet = ` {
|
|
396
|
+
name: '${names.title}',
|
|
397
|
+
path: '${names.route}',
|
|
398
|
+
component: '${names.component}',
|
|
399
|
+
},
|
|
400
|
+
`;
|
|
401
|
+
let next = current;
|
|
402
|
+
if (current.includes("path: '/*'") || current.includes('path: "/*"')) {
|
|
403
|
+
next = current.replace(/\{\s*path:\s*['"]\/\*['"]/, `${snippet} $&`);
|
|
404
|
+
} else {
|
|
405
|
+
next = current.replace(/\];\s*$/, `${snippet}];\n`);
|
|
406
|
+
}
|
|
407
|
+
writeFile(file, next, dryRun);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function generatePage(cwd, options) {
|
|
411
|
+
const operations = [
|
|
412
|
+
['list', options.listOperation],
|
|
413
|
+
['create', options.createOperation],
|
|
414
|
+
['update', options.updateOperation],
|
|
415
|
+
['remove', options.deleteOperation],
|
|
416
|
+
].filter(([, value]) => value);
|
|
417
|
+
|
|
418
|
+
const project = loadProject(cwd, {
|
|
419
|
+
...options,
|
|
420
|
+
requireSpec: operations.length > 0,
|
|
421
|
+
});
|
|
422
|
+
const names = featureNames(options);
|
|
423
|
+
const layout = detectLayout(project.root);
|
|
424
|
+
const pageDir = path.join(project.root, 'src/pages', names.featurePath);
|
|
425
|
+
const files = [
|
|
426
|
+
path.join(pageDir, 'index.tsx'),
|
|
427
|
+
path.join(pageDir, 'index.less'),
|
|
428
|
+
path.join(pageDir, 'const.tsx'),
|
|
429
|
+
path.join(pageDir, 'components', `${names.drawer}.tsx`),
|
|
430
|
+
path.join(pageDir, 'components', `${names.confirm}.tsx`),
|
|
431
|
+
];
|
|
432
|
+
const exists = files.filter((file) => fs.existsSync(file));
|
|
433
|
+
if (exists.length) {
|
|
434
|
+
throw new Error(`Refusing to overwrite existing page files:\n${exists.map((file) => path.relative(project.root, file)).join('\n')}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const fns = {};
|
|
438
|
+
let types = { itemType: `${toPascal(names.leaf)}_API.${names.leaf}Item`, queryType: undefined };
|
|
439
|
+
let pagination = 'list-total';
|
|
440
|
+
let itemSource = '';
|
|
441
|
+
|
|
442
|
+
for (const [key, selector] of operations) {
|
|
443
|
+
const result = generateService(cwd, {
|
|
444
|
+
...options,
|
|
445
|
+
operation: selector,
|
|
446
|
+
mockFile: false,
|
|
447
|
+
});
|
|
448
|
+
fns[key] = result.fn;
|
|
449
|
+
if (key === 'list') {
|
|
450
|
+
types = {
|
|
451
|
+
itemType: result.itemType || types.itemType,
|
|
452
|
+
queryType: result.queryType,
|
|
453
|
+
};
|
|
454
|
+
pagination = result.pagination || pagination;
|
|
455
|
+
itemSource = result.nextTypings || '';
|
|
456
|
+
if (result.status === 'reuse' && result.typingsPath && fs.existsSync(result.typingsPath)) {
|
|
457
|
+
itemSource = fs.readFileSync(result.typingsPath, 'utf8');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const fields = columnFields(itemSource);
|
|
463
|
+
const permissions = {
|
|
464
|
+
create: options.permissionCreate,
|
|
465
|
+
edit: options.permissionEdit,
|
|
466
|
+
delete: options.permissionDelete,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
writeFile(files[0], emitIndex(names, layout, types, fns, pagination), project.dryRun);
|
|
470
|
+
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);
|
|
473
|
+
writeFile(files[4], emitConfirm(names), project.dryRun);
|
|
474
|
+
patchRoute(project.root, names, project.dryRun);
|
|
475
|
+
|
|
476
|
+
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.`);
|
|
478
|
+
}
|
|
479
|
+
console.log(`${project.dryRun ? '[dry-run] ' : ''}generated page ${names.route} -> src/pages/${names.featurePath}`);
|
|
480
|
+
return names;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
module.exports = { generatePage, featureNames };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function readJson(file) {
|
|
5
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function findProjectRoot(start) {
|
|
9
|
+
let current = path.resolve(start);
|
|
10
|
+
while (true) {
|
|
11
|
+
if (fs.existsSync(path.join(current, '.star-allinone.json'))) return current;
|
|
12
|
+
const parent = path.dirname(current);
|
|
13
|
+
if (parent === current) {
|
|
14
|
+
throw new Error('Missing .star-allinone.json. Run this command from a business project root.');
|
|
15
|
+
}
|
|
16
|
+
current = parent;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadProject(cwd, options = {}) {
|
|
21
|
+
const root = findProjectRoot(cwd);
|
|
22
|
+
const config = readJson(path.join(root, '.star-allinone.json'));
|
|
23
|
+
const specPath = options.spec || config.openapi
|
|
24
|
+
? path.resolve(root, options.spec || config.openapi)
|
|
25
|
+
: '';
|
|
26
|
+
if (options.requireSpec !== false && !specPath) {
|
|
27
|
+
throw new Error('Set openapi in .star-allinone.json or pass --spec <file>.');
|
|
28
|
+
}
|
|
29
|
+
if (specPath && !fs.existsSync(specPath)) {
|
|
30
|
+
throw new Error(`OpenAPI file not found: ${specPath}`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
root,
|
|
34
|
+
config,
|
|
35
|
+
specPath,
|
|
36
|
+
spec: specPath ? readJson(specPath) : null,
|
|
37
|
+
serviceDir: path.resolve(root, config.serviceDir || 'src/services'),
|
|
38
|
+
dryRun: Boolean(options.dryRun),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function listDomainDirs(serviceDir) {
|
|
43
|
+
if (!fs.existsSync(serviceDir)) return [];
|
|
44
|
+
return fs.readdirSync(serviceDir, { withFileTypes: true })
|
|
45
|
+
.filter((entry) => entry.isDirectory())
|
|
46
|
+
.map((entry) => entry.name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function existingRequestFile(serviceDir) {
|
|
50
|
+
for (const name of ['request.tsx', 'request.ts']) {
|
|
51
|
+
const file = path.join(serviceDir, name);
|
|
52
|
+
if (fs.existsSync(file)) return name.replace(/\.(tsx|ts)$/, '');
|
|
53
|
+
}
|
|
54
|
+
return 'request';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeFile(file, content, dryRun) {
|
|
58
|
+
if (dryRun) {
|
|
59
|
+
console.log(`[dry-run] write ${file}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
63
|
+
fs.writeFileSync(file, content.endsWith('\n') ? content : `${content}\n`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function typeExists(content, typeName) {
|
|
67
|
+
return new RegExp(`\\btype\\s+${typeName}\\b`).test(content);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function insertTypes(content, namespace, blocks) {
|
|
71
|
+
if (!blocks.length) return content;
|
|
72
|
+
const nsRe = new RegExp(`declare namespace ${namespace}\\s*\\{`);
|
|
73
|
+
if (!nsRe.test(content)) {
|
|
74
|
+
const extra = `declare namespace ${namespace} {\n${blocks.join('\n\n')}\n}\n`;
|
|
75
|
+
return content.trim() ? `${content.trim()}\n\n${extra}` : extra;
|
|
76
|
+
}
|
|
77
|
+
const lastBrace = content.lastIndexOf('}');
|
|
78
|
+
if (lastBrace === -1) throw new Error(`Cannot insert types into ${namespace}`);
|
|
79
|
+
return `${content.slice(0, lastBrace).replace(/\s*$/, '\n')}${blocks.join('\n\n')}\n${content.slice(lastBrace)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function insertMethods(content, methods, exportNames, requestImport) {
|
|
83
|
+
let next = content;
|
|
84
|
+
if (!next.includes("from '../request'") && !next.includes('from "../request"')) {
|
|
85
|
+
next = `import MFRequest from '../${requestImport}';\n${next}`;
|
|
86
|
+
}
|
|
87
|
+
const exportMatch = next.match(/export default \{[\s\S]*?\};?/);
|
|
88
|
+
if (!exportMatch) {
|
|
89
|
+
return `${next.trim()}\n\n${methods.join('\n\n')}\n\nexport default {\n${exportNames.map((name) => ` ${name},`).join('\n')}\n};\n`;
|
|
90
|
+
}
|
|
91
|
+
const insertAt = exportMatch.index;
|
|
92
|
+
const before = next.slice(0, insertAt).replace(/\s*$/, '\n\n');
|
|
93
|
+
const exportBlock = exportMatch[0];
|
|
94
|
+
const namesToAdd = exportNames.filter((name) => !new RegExp(`\\b${name}\\b`).test(exportBlock));
|
|
95
|
+
let updatedExport = exportBlock;
|
|
96
|
+
if (namesToAdd.length) {
|
|
97
|
+
updatedExport = exportBlock.replace(
|
|
98
|
+
/export default \{/,
|
|
99
|
+
`export default {\n${namesToAdd.map((name) => ` ${name},`).join('\n')}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return `${before}${methods.join('\n\n')}\n\n${updatedExport}${next.slice(insertAt + exportBlock.length)}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function patchServiceIndex(indexFile, domain, serviceFile, dryRun) {
|
|
106
|
+
const importName = `${toCamelSafe(domain)}API`;
|
|
107
|
+
const importLine = `import ${importName} from './${domain}/${serviceFile}';`;
|
|
108
|
+
const spread = `...${importName},`;
|
|
109
|
+
if (!fs.existsSync(indexFile)) {
|
|
110
|
+
writeFile(
|
|
111
|
+
indexFile,
|
|
112
|
+
`${importLine}\n\nexport default {\n ${spread}\n};\n`,
|
|
113
|
+
dryRun,
|
|
114
|
+
);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const current = fs.readFileSync(indexFile, 'utf8');
|
|
118
|
+
if (current.includes(`'./${domain}/${serviceFile}'`) || current.includes(`"./${domain}/${serviceFile}"`)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let next = `${importLine}\n${current}`;
|
|
122
|
+
if (/export default \{/.test(next) && !next.includes(spread)) {
|
|
123
|
+
next = next.replace(/export default \{/, `export default {\n ${spread}`);
|
|
124
|
+
}
|
|
125
|
+
writeFile(indexFile, next, dryRun);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toCamelSafe(domain) {
|
|
129
|
+
return domain.charAt(0).toLowerCase() + domain.slice(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function patchRootTypings(root, domain, dryRun) {
|
|
133
|
+
const file = path.join(root, 'typings.d.ts');
|
|
134
|
+
if (!fs.existsSync(file)) return;
|
|
135
|
+
const ref = `/// <reference path="./src/services/${domain}/typings.d.ts" />`;
|
|
136
|
+
const current = fs.readFileSync(file, 'utf8');
|
|
137
|
+
if (current.includes(`src/services/${domain}/typings.d.ts`)) return;
|
|
138
|
+
if (!current.includes('/// <reference path=')) return;
|
|
139
|
+
writeFile(file, `${ref}\n${current}`, dryRun);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
readJson,
|
|
144
|
+
findProjectRoot,
|
|
145
|
+
loadProject,
|
|
146
|
+
listDomainDirs,
|
|
147
|
+
existingRequestFile,
|
|
148
|
+
writeFile,
|
|
149
|
+
typeExists,
|
|
150
|
+
insertTypes,
|
|
151
|
+
insertMethods,
|
|
152
|
+
patchServiceIndex,
|
|
153
|
+
patchRootTypings,
|
|
154
|
+
};
|