@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.
- package/README.md +35 -75
- package/dist/components/CustomDrawer/index.d.ts +2 -0
- package/dist/components/CustomDrawer/index.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +1 -1
- package/dist/tracking/index.d.ts +60 -0
- package/dist/tracking/index.d.ts.map +1 -0
- package/examples/AGENTS.md +14 -2
- package/package.json +4 -2
- package/scripts/cli.js +28 -1
- package/scripts/gen/doctor.js +293 -0
- package/scripts/gen/fields.js +118 -0
- package/scripts/gen/fixtures/run.js +78 -10
- package/scripts/gen/fixtures/widgets.openapi.json +2 -1
- package/scripts/gen/page.js +194 -43
- package/skills/README.md +16 -3
- package/skills/shared/design-to-page/SKILL.md +3 -3
- package/skills/shared/design-to-page/references/list-page-template.md +13 -3
- package/skills/shared/design-to-page/references/overlays.md +6 -0
- package/skills/shared/design-to-page/references/page-conventions.md +2 -1
- package/skills/shared/project-conventions/references/architecture.md +5 -2
- package/skills/shared/project-conventions/references/component-contract.md +38 -0
- package/skills/shared/project-conventions/references/mfprotable.md +59 -0
- package/skills/shared/project-conventions/references/quality-checklist.md +14 -0
- package/skills/shared/project-conventions/references/review-checklist.md +2 -2
- package/skills/shared/publish-test/SKILL.md +10 -0
- package/skills/shared/review-and-submit/references/ai-review.md +5 -1
- package/skills/shared/review-and-submit/references/verify.md +2 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '../..');
|
|
6
|
+
const SHARED_SKILLS = path.join(PACKAGE_ROOT, 'skills', 'shared');
|
|
7
|
+
const DEFAULT_PREFIX = 'shared-';
|
|
8
|
+
const LOCAL_OVERLAYS = ['CustomDrawer', 'BaseModal', 'HocModal'];
|
|
9
|
+
const REQUIRED_CONFIG_KEYS = ['project', 'serviceDir'];
|
|
10
|
+
const KNOWN_CONFIG_KEYS = new Set([
|
|
11
|
+
'project',
|
|
12
|
+
'publicSkillPrefix',
|
|
13
|
+
'serviceDir',
|
|
14
|
+
'openapi',
|
|
15
|
+
'publishPipelineId',
|
|
16
|
+
'sharedSkills',
|
|
17
|
+
'variables',
|
|
18
|
+
]);
|
|
19
|
+
const IGNORE_DIRS = new Set([
|
|
20
|
+
'node_modules',
|
|
21
|
+
'dist',
|
|
22
|
+
'.git',
|
|
23
|
+
'.umi',
|
|
24
|
+
'coverage',
|
|
25
|
+
'.turbo',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function readJson(file) {
|
|
29
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function existsAny(root, relatives) {
|
|
33
|
+
return relatives.find((relative) => fs.existsSync(path.join(root, relative)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function walkFiles(dir, results = []) {
|
|
37
|
+
if (!fs.existsSync(dir)) return results;
|
|
38
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
39
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
40
|
+
const full = path.join(dir, entry.name);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
walkFiles(full, results);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) results.push(full);
|
|
46
|
+
}
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const TEMPLATE_EXT = new Set(['.md', '.json', '.yml', '.yaml', '.txt']);
|
|
51
|
+
|
|
52
|
+
function render(content, variables) {
|
|
53
|
+
return content.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (_, key) => {
|
|
54
|
+
const value = key.split('.').reduce((current, part) => current?.[part], variables);
|
|
55
|
+
return value == null ? `{{${key}}}` : String(value);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hashTree(dir, variables) {
|
|
60
|
+
if (!fs.existsSync(dir)) return '';
|
|
61
|
+
const files = [];
|
|
62
|
+
const walk = (current, relative) => {
|
|
63
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
64
|
+
const nextRel = relative ? `${relative}/${entry.name}` : entry.name;
|
|
65
|
+
const full = path.join(current, entry.name);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
walk(full, nextRel);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let content = fs.readFileSync(full);
|
|
71
|
+
if (variables && TEMPLATE_EXT.has(path.extname(entry.name).toLowerCase())) {
|
|
72
|
+
content = Buffer.from(render(content.toString('utf8'), variables));
|
|
73
|
+
}
|
|
74
|
+
files.push({ rel: nextRel, content });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(dir, '');
|
|
78
|
+
files.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
79
|
+
const digest = crypto.createHash('sha256');
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
digest.update(file.rel);
|
|
82
|
+
digest.update('\0');
|
|
83
|
+
digest.update(file.content);
|
|
84
|
+
digest.update('\0');
|
|
85
|
+
}
|
|
86
|
+
return digest.digest('hex');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function listPackageSkills() {
|
|
90
|
+
if (!fs.existsSync(SHARED_SKILLS)) return [];
|
|
91
|
+
return fs.readdirSync(SHARED_SKILLS, { withFileTypes: true })
|
|
92
|
+
.filter((entry) => entry.isDirectory())
|
|
93
|
+
.map((entry) => entry.name)
|
|
94
|
+
.sort();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function check(ok, level, message) {
|
|
98
|
+
return { ok, level, message };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runDoctor(cwd, options = {}) {
|
|
102
|
+
const root = path.resolve(options.target || cwd);
|
|
103
|
+
const findings = [];
|
|
104
|
+
const configFile = path.join(root, '.star-allinone.json');
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(configFile)) {
|
|
107
|
+
findings.push(check(false, 'error', `Missing ${path.relative(root, configFile)}`));
|
|
108
|
+
printReport(root, findings);
|
|
109
|
+
return findings;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let config;
|
|
113
|
+
try {
|
|
114
|
+
config = readJson(configFile);
|
|
115
|
+
findings.push(check(true, 'ok', '.star-allinone.json'));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
findings.push(check(false, 'error', `.star-allinone.json is not valid JSON: ${error.message}`));
|
|
118
|
+
printReport(root, findings);
|
|
119
|
+
return findings;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const unknownKeys = Object.keys(config).filter((key) => !KNOWN_CONFIG_KEYS.has(key));
|
|
123
|
+
if (unknownKeys.length) {
|
|
124
|
+
findings.push(check(false, 'warn', `Unknown config keys: ${unknownKeys.join(', ')}`));
|
|
125
|
+
}
|
|
126
|
+
for (const key of REQUIRED_CONFIG_KEYS) {
|
|
127
|
+
if (!config[key]) {
|
|
128
|
+
findings.push(check(false, 'error', `Config missing required "${key}"`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const serviceDir = config.serviceDir || 'src/services';
|
|
133
|
+
if (!fs.existsSync(path.join(root, serviceDir))) {
|
|
134
|
+
findings.push(check(false, 'error', `serviceDir not found: ${serviceDir}`));
|
|
135
|
+
} else {
|
|
136
|
+
findings.push(check(true, 'ok', `serviceDir ${serviceDir}`));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const requestFile = existsAny(root, [
|
|
140
|
+
`${serviceDir}/request.tsx`,
|
|
141
|
+
`${serviceDir}/request.ts`,
|
|
142
|
+
]);
|
|
143
|
+
if (!requestFile) {
|
|
144
|
+
findings.push(check(false, 'error', `MFRequest helper missing under ${serviceDir}/request.(ts|tsx)`));
|
|
145
|
+
} else {
|
|
146
|
+
const content = fs.readFileSync(path.join(root, requestFile), 'utf8');
|
|
147
|
+
if (!content.includes('MFRequest')) {
|
|
148
|
+
findings.push(check(false, 'error', `${requestFile} does not define MFRequest`));
|
|
149
|
+
} else {
|
|
150
|
+
findings.push(check(true, 'ok', requestFile));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (config.openapi) {
|
|
155
|
+
if (fs.existsSync(path.join(root, config.openapi))) {
|
|
156
|
+
findings.push(check(true, 'ok', `openapi ${config.openapi}`));
|
|
157
|
+
} else {
|
|
158
|
+
findings.push(check(false, 'error', `openapi file not found: ${config.openapi}`));
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
findings.push(check(false, 'error', 'Config missing "openapi"'));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!config.publishPipelineId) {
|
|
165
|
+
findings.push(check(false, 'warn', 'Config missing "publishPipelineId"; $shared-publish-test cannot run'));
|
|
166
|
+
} else {
|
|
167
|
+
findings.push(check(true, 'ok', `publishPipelineId ${config.publishPipelineId}`));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const tableFile = existsAny(root, [
|
|
171
|
+
'src/components/CommonTable/MFProTable.tsx',
|
|
172
|
+
'src/components/CommonTable/MFProTable.ts',
|
|
173
|
+
]);
|
|
174
|
+
if (!tableFile) {
|
|
175
|
+
findings.push(check(false, 'error', 'MFProTable missing at src/components/CommonTable/MFProTable.tsx (app-local, not this package)'));
|
|
176
|
+
} else {
|
|
177
|
+
findings.push(check(true, 'ok', tableFile));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!fs.existsSync(path.join(root, 'AGENTS.md'))) {
|
|
181
|
+
findings.push(check(false, 'warn', 'Missing AGENTS.md; run skills sync once to seed it'));
|
|
182
|
+
} else {
|
|
183
|
+
findings.push(check(true, 'ok', 'AGENTS.md'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const prefix = config.publicSkillPrefix || DEFAULT_PREFIX;
|
|
187
|
+
const skillsDir = path.join(root, '.agents', 'skills');
|
|
188
|
+
const stampFile = path.join(skillsDir, '.star-allinone-sync.json');
|
|
189
|
+
const packageJson = readJson(path.join(PACKAGE_ROOT, 'package.json'));
|
|
190
|
+
if (fs.existsSync(stampFile)) {
|
|
191
|
+
try {
|
|
192
|
+
const stamp = readJson(stampFile);
|
|
193
|
+
if (stamp.packageVersion && stamp.packageVersion !== packageJson.version) {
|
|
194
|
+
findings.push(check(false, 'warn', `skills synced from ${stamp.packageVersion}, package is ${packageJson.version}. Run skills sync`));
|
|
195
|
+
} else {
|
|
196
|
+
findings.push(check(true, 'ok', `skills sync stamp ${stamp.packageVersion || packageJson.version}`));
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
findings.push(check(false, 'warn', `.star-allinone-sync.json is not valid JSON: ${error.message}`));
|
|
200
|
+
}
|
|
201
|
+
} else if (fs.existsSync(skillsDir)) {
|
|
202
|
+
findings.push(check(false, 'warn', 'Missing .agents/skills/.star-allinone-sync.json; run skills sync'));
|
|
203
|
+
}
|
|
204
|
+
const packageSkills = listPackageSkills();
|
|
205
|
+
if (!fs.existsSync(skillsDir)) {
|
|
206
|
+
findings.push(check(false, 'error', 'Missing .agents/skills. Run pnpm exec star-allinone skills sync'));
|
|
207
|
+
for (const skill of packageSkills) {
|
|
208
|
+
findings.push(check(false, 'error', `Shared skill not synced: ${prefix}${skill}. Run pnpm exec star-allinone skills sync`));
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
for (const skill of packageSkills) {
|
|
212
|
+
const publicName = `${prefix}${skill}`;
|
|
213
|
+
const dest = path.join(skillsDir, publicName);
|
|
214
|
+
const source = path.join(SHARED_SKILLS, skill);
|
|
215
|
+
if (!fs.existsSync(dest)) {
|
|
216
|
+
findings.push(check(false, 'error', `Shared skill not synced: ${publicName}. Run pnpm exec star-allinone skills sync`));
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const variables = {
|
|
220
|
+
...config,
|
|
221
|
+
...config.variables,
|
|
222
|
+
sharedSkillName: publicName,
|
|
223
|
+
project: { ...config, ...config.variables },
|
|
224
|
+
};
|
|
225
|
+
if (hashTree(source, variables) !== hashTree(dest)) {
|
|
226
|
+
findings.push(check(false, 'warn', `${publicName} is stale vs this package. Run pnpm exec star-allinone skills sync`));
|
|
227
|
+
} else {
|
|
228
|
+
findings.push(check(true, 'ok', publicName));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const pkgFile = path.join(root, 'package.json');
|
|
234
|
+
if (fs.existsSync(pkgFile)) {
|
|
235
|
+
const pkg = readJson(pkgFile);
|
|
236
|
+
const dep = pkg.dependencies?.['@mingfuyun/star-allinone']
|
|
237
|
+
|| pkg.devDependencies?.['@mingfuyun/star-allinone'];
|
|
238
|
+
if (!dep) {
|
|
239
|
+
findings.push(check(false, 'warn', 'package.json does not depend on @mingfuyun/star-allinone'));
|
|
240
|
+
} else {
|
|
241
|
+
findings.push(check(true, 'ok', `@mingfuyun/star-allinone ${dep}`));
|
|
242
|
+
}
|
|
243
|
+
const testScript = pkg.scripts?.test || '';
|
|
244
|
+
if (/\bmax\s+dev\b/.test(testScript)) {
|
|
245
|
+
findings.push(check(false, 'warn', 'package.json "test" runs max dev. Rename it to start:test / dev:test so agents do not treat it as unit tests'));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const overlay of LOCAL_OVERLAYS) {
|
|
250
|
+
const overlayDir = path.join(root, 'src/components', overlay);
|
|
251
|
+
if (fs.existsSync(overlayDir)) {
|
|
252
|
+
findings.push(check(false, 'error', `Local ${overlay} at src/components/${overlay} should be removed; import from @mingfuyun/star-allinone`));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const files = walkFiles(path.join(root, 'src'));
|
|
257
|
+
const badTable = [];
|
|
258
|
+
const localOverlay = [];
|
|
259
|
+
const tableFromPackage = /import\s+(?:type\s+)?(?:\{[^}]*\bMF(?:TablePro|ProTable)\b[^}]*\}|MF(?:TablePro|ProTable))\s+from\s+['"]@mingfuyun\/star-allinone['"]/;
|
|
260
|
+
const localDrawer = /from\s+['"]@\/components\/(?:CustomDrawer|BaseModal|HocModal)(?:\/[^'"]*)?['"]/;
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
263
|
+
const relative = path.relative(root, file);
|
|
264
|
+
if (tableFromPackage.test(content)) badTable.push(relative);
|
|
265
|
+
if (localDrawer.test(content)) localOverlay.push(relative);
|
|
266
|
+
}
|
|
267
|
+
if (badTable.length) {
|
|
268
|
+
findings.push(check(false, 'error', `MFProTable/MFTablePro imported from the npm package in ${badTable.join(', ')}. Use @/components/CommonTable/MFProTable`));
|
|
269
|
+
} else {
|
|
270
|
+
findings.push(check(true, 'ok', 'No table imports from @mingfuyun/star-allinone'));
|
|
271
|
+
}
|
|
272
|
+
if (localOverlay.length) {
|
|
273
|
+
findings.push(check(false, 'error', `Local overlay imports (use @mingfuyun/star-allinone): ${localOverlay.join(', ')}`));
|
|
274
|
+
} else {
|
|
275
|
+
findings.push(check(true, 'ok', 'Overlay imports use @mingfuyun/star-allinone or none found'));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
printReport(root, findings);
|
|
279
|
+
return findings;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function printReport(root, findings) {
|
|
283
|
+
console.log(`star-allinone doctor (${root})`);
|
|
284
|
+
for (const item of findings) {
|
|
285
|
+
const tag = item.level === 'ok' ? 'OK ' : item.level === 'warn' ? 'WARN' : 'ERR ';
|
|
286
|
+
console.log(`${tag} ${item.message}`);
|
|
287
|
+
}
|
|
288
|
+
const errors = findings.filter((item) => item.level === 'error').length;
|
|
289
|
+
const warnings = findings.filter((item) => item.level === 'warn').length;
|
|
290
|
+
console.log(`\n${errors} error(s), ${warnings} warning(s)`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = { runDoctor, hashTree };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const { deref } = require('./spec');
|
|
2
|
+
|
|
3
|
+
const SKIP_COLUMNS = new Set([
|
|
4
|
+
'created_at',
|
|
5
|
+
'updated_at',
|
|
6
|
+
'createTime',
|
|
7
|
+
'updateTime',
|
|
8
|
+
'deleted_at',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const SKIP_FORM = new Set([
|
|
12
|
+
'id',
|
|
13
|
+
'pk',
|
|
14
|
+
'created_at',
|
|
15
|
+
'updated_at',
|
|
16
|
+
'createTime',
|
|
17
|
+
'updateTime',
|
|
18
|
+
'deleted_at',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const SKIP_SEARCH = new Set([
|
|
22
|
+
'page',
|
|
23
|
+
'page_size',
|
|
24
|
+
'pageSize',
|
|
25
|
+
'current',
|
|
26
|
+
'limit',
|
|
27
|
+
'offset',
|
|
28
|
+
'ordering',
|
|
29
|
+
'sort',
|
|
30
|
+
'sort_by',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function schemaType(schema) {
|
|
34
|
+
if (!schema) return undefined;
|
|
35
|
+
if (schema.enum?.length) return 'string';
|
|
36
|
+
const types = Array.isArray(schema.type)
|
|
37
|
+
? schema.type.filter((item) => item !== 'null')
|
|
38
|
+
: [schema.type];
|
|
39
|
+
return types[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fieldFromSchema(spec, name, schema, required) {
|
|
43
|
+
const resolved = deref(spec, schema) || {};
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
title: resolved.title || name,
|
|
47
|
+
required: Boolean(required),
|
|
48
|
+
type: schemaType(resolved),
|
|
49
|
+
format: resolved.format,
|
|
50
|
+
enum: Array.isArray(resolved.enum) ? resolved.enum : undefined,
|
|
51
|
+
readOnly: Boolean(resolved.readOnly),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function schemaFields(spec, schema, options = {}) {
|
|
56
|
+
if (!spec || !schema) return [];
|
|
57
|
+
const skip = options.skip || SKIP_COLUMNS;
|
|
58
|
+
const resolved = deref(spec, schema);
|
|
59
|
+
const properties = resolved?.properties || {};
|
|
60
|
+
const required = new Set(resolved?.required || []);
|
|
61
|
+
const fields = [];
|
|
62
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
63
|
+
if (skip.has(name)) continue;
|
|
64
|
+
const field = fieldFromSchema(spec, name, prop, required.has(name));
|
|
65
|
+
if (options.forForm && field.readOnly) continue;
|
|
66
|
+
fields.push(field);
|
|
67
|
+
}
|
|
68
|
+
if (typeof options.limit === 'number') return fields.slice(0, options.limit);
|
|
69
|
+
return fields;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formComponent(field) {
|
|
73
|
+
if (field.enum?.length) return 'ProFormSelect';
|
|
74
|
+
if (field.type === 'boolean') return 'ProFormSwitch';
|
|
75
|
+
if (field.type === 'integer' || field.type === 'number') return 'ProFormDigit';
|
|
76
|
+
if (field.format === 'date-time') return 'ProFormDateTimePicker';
|
|
77
|
+
if (field.format === 'date') return 'ProFormDatePicker';
|
|
78
|
+
if (/desc|note|remark|comment|content|message/i.test(field.name)) return 'ProFormTextArea';
|
|
79
|
+
return 'ProFormText';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function columnValueType(field) {
|
|
83
|
+
if (field.enum?.length) return 'select';
|
|
84
|
+
if (field.type === 'boolean') return 'select';
|
|
85
|
+
if (field.type === 'integer' || field.type === 'number') return 'digit';
|
|
86
|
+
if (field.format === 'date-time') return 'dateTime';
|
|
87
|
+
if (field.format === 'date') return 'date';
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fallbackFields(itemTypeSource = '') {
|
|
92
|
+
const names = [...itemTypeSource.matchAll(/^\s+(\w+)\??:/gm)]
|
|
93
|
+
.map((match) => match[1])
|
|
94
|
+
.filter((name) => !SKIP_COLUMNS.has(name));
|
|
95
|
+
return names.slice(0, 6).map((name) => ({
|
|
96
|
+
name,
|
|
97
|
+
title: name,
|
|
98
|
+
required: false,
|
|
99
|
+
type: 'string',
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function querySearchNames(queryParams = []) {
|
|
104
|
+
return queryParams
|
|
105
|
+
.map((param) => param?.name)
|
|
106
|
+
.filter((name) => name && !SKIP_SEARCH.has(name));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
SKIP_COLUMNS,
|
|
111
|
+
SKIP_FORM,
|
|
112
|
+
SKIP_SEARCH,
|
|
113
|
+
schemaFields,
|
|
114
|
+
formComponent,
|
|
115
|
+
columnValueType,
|
|
116
|
+
fallbackFields,
|
|
117
|
+
querySearchNames,
|
|
118
|
+
};
|
|
@@ -3,6 +3,7 @@ const os = require('os');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { generateService } = require('../service');
|
|
5
5
|
const { generatePage } = require('../page');
|
|
6
|
+
const { runDoctor } = require('../doctor');
|
|
6
7
|
|
|
7
8
|
function assert(condition, message) {
|
|
8
9
|
if (!condition) throw new Error(message);
|
|
@@ -15,6 +16,7 @@ function makeProject() {
|
|
|
15
16
|
JSON.stringify(
|
|
16
17
|
{
|
|
17
18
|
project: 'fixture',
|
|
19
|
+
publicSkillPrefix: 'shared-',
|
|
18
20
|
serviceDir: 'src/services',
|
|
19
21
|
openapi: 'openapi.json',
|
|
20
22
|
},
|
|
@@ -45,10 +47,41 @@ function makeProject() {
|
|
|
45
47
|
`,
|
|
46
48
|
);
|
|
47
49
|
fs.mkdirSync(path.join(root, 'src/components/CommonTable'), { recursive: true });
|
|
50
|
+
fs.writeFileSync(path.join(root, 'src/components/CommonTable/MFProTable.tsx'), 'export default function MFProTable() { return null; }\n');
|
|
48
51
|
fs.mkdirSync(path.join(root, 'src/pages'), { recursive: true });
|
|
52
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({
|
|
53
|
+
dependencies: { '@mingfuyun/star-allinone': '^0.3.2' },
|
|
54
|
+
scripts: { typecheck: 'tsc --noEmit' },
|
|
55
|
+
}, null, 2));
|
|
49
56
|
return root;
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
function writeContainerConfig(root) {
|
|
60
|
+
const dir = path.join(root, 'src/components/Page/Container');
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
fs.writeFileSync(
|
|
63
|
+
path.join(dir, 'config.tsx'),
|
|
64
|
+
`export default {
|
|
65
|
+
'/existing': [
|
|
66
|
+
{ key: 'tab-existing', label: 'Existing', loader: () => import('@/pages/Existing/components/List') },
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function generateWidgetsPage(root, feature = 'Widgets', route = '/widgets') {
|
|
74
|
+
generatePage(root, {
|
|
75
|
+
feature,
|
|
76
|
+
route,
|
|
77
|
+
title: '组件管理',
|
|
78
|
+
listOperation: 'GET /api/v1/widgets/',
|
|
79
|
+
createOperation: 'POST /api/v1/widgets/',
|
|
80
|
+
updateOperation: 'PATCH /api/v1/widgets/{id}/',
|
|
81
|
+
deleteOperation: 'DELETE /api/v1/widgets/{id}/',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
52
85
|
function main() {
|
|
53
86
|
const root = makeProject();
|
|
54
87
|
const first = generateService(root, {
|
|
@@ -78,21 +111,56 @@ function main() {
|
|
|
78
111
|
assert(patched.includes('widgetsPartialUpdate'), patched);
|
|
79
112
|
assert(patched.includes('MFRequest.patch<WIDGETS_API.Widget>'), patched);
|
|
80
113
|
|
|
81
|
-
|
|
82
|
-
feature: 'Widgets',
|
|
83
|
-
route: '/widgets',
|
|
84
|
-
title: '组件管理',
|
|
85
|
-
listOperation: 'GET /api/v1/widgets/',
|
|
86
|
-
createOperation: 'POST /api/v1/widgets/',
|
|
87
|
-
updateOperation: 'PATCH /api/v1/widgets/{id}/',
|
|
88
|
-
deleteOperation: 'DELETE /api/v1/widgets/{id}/',
|
|
89
|
-
});
|
|
114
|
+
generateWidgetsPage(root);
|
|
90
115
|
const page = fs.readFileSync(path.join(root, 'src/pages/Widgets/index.tsx'), 'utf8');
|
|
91
116
|
assert(page.includes('Service.widgetsList'), page);
|
|
92
|
-
assert(page.includes(
|
|
117
|
+
assert(page.includes('rowKey="id"'), page);
|
|
118
|
+
const columns = fs.readFileSync(path.join(root, 'src/pages/Widgets/const.tsx'), 'utf8');
|
|
119
|
+
assert(columns.includes("dataIndex: 'name'"), columns);
|
|
120
|
+
assert(columns.includes("dataIndex: 'kind'"), columns);
|
|
121
|
+
assert(columns.includes("valueType: 'select'"), columns);
|
|
122
|
+
assert(columns.includes("valueType: 'digit'"), columns);
|
|
123
|
+
const drawer = fs.readFileSync(path.join(root, 'src/pages/Widgets/components/WidgetsDrawer.tsx'), 'utf8');
|
|
124
|
+
assert(drawer.includes('ProFormSelect'), drawer);
|
|
125
|
+
assert(drawer.includes("name=\"status\""), drawer);
|
|
126
|
+
assert(drawer.includes("name=\"name\""), drawer);
|
|
127
|
+
assert(drawer.includes('required: true'), drawer);
|
|
93
128
|
const route = fs.readFileSync(path.join(root, 'config/route.ts'), 'utf8');
|
|
94
129
|
assert(route.includes("path: '/widgets'"), route);
|
|
130
|
+
|
|
131
|
+
const tabRoot = makeProject();
|
|
132
|
+
writeContainerConfig(tabRoot);
|
|
133
|
+
generateService(tabRoot, { operation: 'GET /api/v1/widgets/' });
|
|
134
|
+
generateWidgetsPage(tabRoot);
|
|
135
|
+
const shell = fs.readFileSync(path.join(tabRoot, 'src/pages/Widgets/index.tsx'), 'utf8');
|
|
136
|
+
assert(shell.includes("import Container from '@/components/Page/Container'"), shell);
|
|
137
|
+
const list = fs.readFileSync(path.join(tabRoot, 'src/pages/Widgets/components/List.tsx'), 'utf8');
|
|
138
|
+
assert(list.includes('Service.widgetsList'), list);
|
|
139
|
+
const tabConfig = fs.readFileSync(path.join(tabRoot, 'src/components/Page/Container/config.tsx'), 'utf8');
|
|
140
|
+
assert(tabConfig.includes("'/widgets'"), tabConfig);
|
|
141
|
+
assert(tabConfig.includes("@/pages/Widgets/components/List"), tabConfig);
|
|
142
|
+
assert(tabConfig.includes("loader: () => import('@/pages/Widgets/components/List')"), tabConfig);
|
|
143
|
+
assert(!tabConfig.includes('children: <Widgets'), tabConfig);
|
|
144
|
+
assert(!/import Widgets from/.test(tabConfig), tabConfig);
|
|
145
|
+
|
|
146
|
+
const healthy = runDoctor(root);
|
|
147
|
+
assert(healthy.some((item) => item.message.includes('MFProTable')), JSON.stringify(healthy));
|
|
148
|
+
assert(!healthy.some((item) => item.level === 'error' && item.message.includes('MFRequest')), JSON.stringify(healthy));
|
|
149
|
+
|
|
150
|
+
fs.mkdirSync(path.join(root, 'src/pages/Bad'), { recursive: true });
|
|
151
|
+
fs.writeFileSync(
|
|
152
|
+
path.join(root, 'src/pages/Bad/index.tsx'),
|
|
153
|
+
"import { MFTablePro } from '@mingfuyun/star-allinone';\nimport CustomDrawer from '@/components/CustomDrawer';\n",
|
|
154
|
+
);
|
|
155
|
+
fs.mkdirSync(path.join(root, 'src/components/CustomDrawer'), { recursive: true });
|
|
156
|
+
fs.writeFileSync(path.join(root, 'src/components/CustomDrawer/index.tsx'), 'export default function CustomDrawer() { return null; }\n');
|
|
157
|
+
const sick = runDoctor(root);
|
|
158
|
+
assert(sick.some((item) => item.level === 'error' && /MFTablePro/.test(item.message)), JSON.stringify(sick));
|
|
159
|
+
assert(sick.some((item) => item.level === 'error' && /Local overlay/.test(item.message)), JSON.stringify(sick));
|
|
160
|
+
|
|
95
161
|
fs.rmSync(root, { recursive: true, force: true });
|
|
162
|
+
fs.rmSync(tabRoot, { recursive: true, force: true });
|
|
163
|
+
process.exitCode = 0;
|
|
96
164
|
console.log('codegen fixtures passed');
|
|
97
165
|
}
|
|
98
166
|
|
|
@@ -165,6 +165,7 @@
|
|
|
165
165
|
"properties": {
|
|
166
166
|
"id": { "type": "string" },
|
|
167
167
|
"name": { "type": "string" },
|
|
168
|
+
"kind": { "type": "string", "enum": ["gadget", "tool"] },
|
|
168
169
|
"status": { "type": "string", "nullable": true },
|
|
169
170
|
"count": { "type": "integer" }
|
|
170
171
|
}
|
|
@@ -174,7 +175,7 @@
|
|
|
174
175
|
"required": ["name"],
|
|
175
176
|
"properties": {
|
|
176
177
|
"name": { "type": "string" },
|
|
177
|
-
"status": { "type": "string" }
|
|
178
|
+
"status": { "type": "string", "enum": ["active", "inactive"] }
|
|
178
179
|
}
|
|
179
180
|
},
|
|
180
181
|
"WidgetList": {
|