@rinova/playground-vite 0.1.5 → 0.7.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/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +508 -97
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +106 -3
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +530 -33
- package/dist/manifest.js.map +1 -1
- package/dist/module-api.d.ts +71 -0
- package/dist/module-api.d.ts.map +1 -0
- package/dist/module-api.js +177 -0
- package/dist/module-api.js.map +1 -0
- package/dist/plugin-spec.d.ts +8 -0
- package/dist/plugin-spec.d.ts.map +1 -0
- package/dist/plugin-spec.js +24 -0
- package/dist/plugin-spec.js.map +1 -0
- package/package.json +1 -1
package/dist/manifest.js
CHANGED
|
@@ -65,11 +65,202 @@ const findComponentFiles = (root, patterns) => {
|
|
|
65
65
|
const componentName = (rel) => path.basename(rel, '.vue');
|
|
66
66
|
/** 去掉 Vue 透传 props 与 onVue:* 钩子 */
|
|
67
67
|
const isOwnProp = (name) => !BUILTIN_PROPS.has(name) && !name.startsWith('onVue:');
|
|
68
|
+
const FLAG_OBJECT = 524288; // ts.TypeFlags.Object
|
|
69
|
+
const FLAG_UNION = 1048576; // ts.TypeFlags.Union
|
|
70
|
+
/**
|
|
71
|
+
* 展开对象 / 对象数组类型的字段(一层)。
|
|
72
|
+
* vue-component-meta 的 schema 只是类型名字符串,这里借 getTypeObject() 拿 TS 类型对象自己走一遍。
|
|
73
|
+
*/
|
|
74
|
+
const DEBUG_FIELDS = Boolean(process.env.RINOVA_DEBUG_FIELDS);
|
|
75
|
+
const readFields = (prop) => {
|
|
76
|
+
try {
|
|
77
|
+
if (DEBUG_FIELDS)
|
|
78
|
+
console.warn('[fields] enter', prop.name);
|
|
79
|
+
const getter = prop.getTypeObject;
|
|
80
|
+
if (typeof getter !== 'function') {
|
|
81
|
+
if (DEBUG_FIELDS)
|
|
82
|
+
console.warn('[fields] no getTypeObject');
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const type = getter.call(prop);
|
|
86
|
+
if (!type) {
|
|
87
|
+
if (DEBUG_FIELDS)
|
|
88
|
+
console.warn('[fields] no type');
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
if (DEBUG_FIELDS)
|
|
92
|
+
console.warn('[fields] type flags', Number(type.flags), 'keys', Object.keys(type).slice(0, 8).join(','));
|
|
93
|
+
const ts = type.checker;
|
|
94
|
+
if (!ts || typeof ts.getPropertiesOfType !== 'function') {
|
|
95
|
+
if (DEBUG_FIELDS)
|
|
96
|
+
console.warn('[fields] no checker', Object.keys(type));
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
let target = type;
|
|
100
|
+
if ((Number(type.flags) & FLAG_UNION) === FLAG_UNION) {
|
|
101
|
+
const parts = type.types ?? [];
|
|
102
|
+
const objectPart = parts.find((part) => (Number(part.flags) & FLAG_OBJECT) === FLAG_OBJECT);
|
|
103
|
+
if (!objectPart) {
|
|
104
|
+
if (DEBUG_FIELDS)
|
|
105
|
+
console.warn('[fields] union without object', parts.map((part) => Number(part.flags)).join(','));
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
target = objectPart;
|
|
109
|
+
}
|
|
110
|
+
// 数组:取元素类型
|
|
111
|
+
const args = target.typeArguments;
|
|
112
|
+
if (args && args.length)
|
|
113
|
+
target = args[0];
|
|
114
|
+
if ((Number(target.flags) & FLAG_OBJECT) !== FLAG_OBJECT) {
|
|
115
|
+
if (DEBUG_FIELDS)
|
|
116
|
+
console.warn('[fields] target not object', Number(target.flags), ts.typeToString(target));
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const symbols = ts.getPropertiesOfType(target);
|
|
120
|
+
return symbols
|
|
121
|
+
.map((symbol) => {
|
|
122
|
+
const name = symbol.getName();
|
|
123
|
+
if (name.startsWith('__'))
|
|
124
|
+
return null;
|
|
125
|
+
const declarations = symbol.declarations;
|
|
126
|
+
const location = (symbol.valueDeclaration ?? declarations?.[0]);
|
|
127
|
+
const symbolType = ts.getTypeOfSymbolAtLocation(symbol, location);
|
|
128
|
+
const text = ts.typeToString(symbolType);
|
|
129
|
+
const docs = symbol.getDocumentationComment(ts);
|
|
130
|
+
const description = Array.isArray(docs)
|
|
131
|
+
? docs.map((part) => part.text ?? '').join('').trim()
|
|
132
|
+
: '';
|
|
133
|
+
return {
|
|
134
|
+
name,
|
|
135
|
+
type: text,
|
|
136
|
+
// 可选性以类型里的 undefined 为准:vue-component-meta 包了一层 symbol,flags 不可靠
|
|
137
|
+
required: !/\|\s*undefined$/.test(text),
|
|
138
|
+
description,
|
|
139
|
+
};
|
|
140
|
+
})
|
|
141
|
+
.filter((item) => item !== null);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (DEBUG_FIELDS)
|
|
145
|
+
console.warn('[fields] error', String(error));
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
/** 事件:从参数元组里找第一个对象参数,展开它的字段 */
|
|
150
|
+
const readEventFields = (event) => {
|
|
151
|
+
try {
|
|
152
|
+
const getter = event.getTypeObject;
|
|
153
|
+
if (typeof getter !== 'function')
|
|
154
|
+
return [];
|
|
155
|
+
const tuple = getter.call(event);
|
|
156
|
+
if (!tuple)
|
|
157
|
+
return [];
|
|
158
|
+
const ts = tuple.checker;
|
|
159
|
+
if (!ts || typeof ts.getPropertiesOfType !== 'function')
|
|
160
|
+
return [];
|
|
161
|
+
const args = tuple.typeArguments ?? [];
|
|
162
|
+
for (const arg of args) {
|
|
163
|
+
const type = arg;
|
|
164
|
+
if ((Number(type.flags) & FLAG_OBJECT) !== FLAG_OBJECT)
|
|
165
|
+
continue;
|
|
166
|
+
const fields = readFields({
|
|
167
|
+
getTypeObject: () => type,
|
|
168
|
+
});
|
|
169
|
+
if (fields.length)
|
|
170
|
+
return fields;
|
|
171
|
+
}
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (DEBUG_FIELDS)
|
|
176
|
+
console.warn('[fields] event error', String(error));
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
};
|
|
68
180
|
const normalizeType = (value) => {
|
|
69
181
|
if (Array.isArray(value))
|
|
70
182
|
return value.map(String).join(' | ');
|
|
71
183
|
return String(value ?? '');
|
|
72
184
|
};
|
|
185
|
+
const FLAG_SYMBOL_INTERFACE = 64; // ts.SymbolFlags.Interface
|
|
186
|
+
const FLAG_SYMBOL_TYPEALIAS = 524288; // ts.SymbolFlags.TypeAlias
|
|
187
|
+
/** 容器类型:取元素类型继续回溯 */
|
|
188
|
+
const WRAPPER_TYPE_NAMES = new Set(['Array', 'ReadonlyArray', 'Set', 'ReadonlySet', 'Map', 'ReadonlyMap', 'Promise']);
|
|
189
|
+
/** 第三方包与 TS lib 里的类型不收录(Array / Record / HTMLElement 之类,读了也只是噪音) */
|
|
190
|
+
const isExternalDeclaration = (file) => /[/\\]node_modules[/\\]/.test(file) || /[/\\]lib\.[a-z0-9.]+\.d\.ts$/.test(file);
|
|
191
|
+
/** 取 schema 项上的 TS 类型对象,拿不到就返回 undefined */
|
|
192
|
+
const typeObjectOf = (value) => {
|
|
193
|
+
try {
|
|
194
|
+
const getter = value.getTypeObject;
|
|
195
|
+
return typeof getter === 'function' ? getter.call(value) : undefined;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (DEBUG_FIELDS)
|
|
199
|
+
console.warn('[types] getTypeObject error', String(error));
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* 从类型对象回溯到具名 interface / type,切出声明原文(含 JSDoc)。
|
|
205
|
+
*
|
|
206
|
+
* 类型字符串(`TableColumn[]`)拿不到结构,只能顺着类型对象往下走:联合去掉 undefined、
|
|
207
|
+
* 容器取元素类型,直到命中本项目的 interface / type 符号。只回溯一层,避免递归整个类型图。
|
|
208
|
+
*/
|
|
209
|
+
const readTypeDeclaration = (type, root) => {
|
|
210
|
+
try {
|
|
211
|
+
let current = type;
|
|
212
|
+
for (let depth = 0; depth < 5 && current; depth += 1) {
|
|
213
|
+
const flags = Number(current.flags);
|
|
214
|
+
if ((flags & FLAG_UNION) === FLAG_UNION) {
|
|
215
|
+
const parts = current.types ?? [];
|
|
216
|
+
current = parts.find((part) => (Number(part.flags) & FLAG_OBJECT) === FLAG_OBJECT);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if ((flags & FLAG_OBJECT) !== FLAG_OBJECT)
|
|
220
|
+
return null;
|
|
221
|
+
const getSymbol = current.getSymbol;
|
|
222
|
+
const symbol = (typeof getSymbol === 'function' ? getSymbol.call(current) : undefined) ??
|
|
223
|
+
current.symbol;
|
|
224
|
+
if (!symbol)
|
|
225
|
+
return null;
|
|
226
|
+
const name = symbol.getName();
|
|
227
|
+
const args = current.typeArguments;
|
|
228
|
+
if (WRAPPER_TYPE_NAMES.has(name) && args?.length) {
|
|
229
|
+
current = args[0];
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const symbolFlags = Number(symbol.flags);
|
|
233
|
+
const isInterface = (symbolFlags & FLAG_SYMBOL_INTERFACE) === FLAG_SYMBOL_INTERFACE;
|
|
234
|
+
if (!isInterface && (symbolFlags & FLAG_SYMBOL_TYPEALIAS) !== FLAG_SYMBOL_TYPEALIAS)
|
|
235
|
+
return null;
|
|
236
|
+
const declarations = symbol.declarations;
|
|
237
|
+
const node = declarations?.[0];
|
|
238
|
+
if (!node)
|
|
239
|
+
return null;
|
|
240
|
+
const sourceFile = node.getSourceFile();
|
|
241
|
+
if (isExternalDeclaration(sourceFile.fileName))
|
|
242
|
+
return null;
|
|
243
|
+
// getFullStart 在声明前留出 JSDoc 的整体注释,trim 后正好从 /** 开始
|
|
244
|
+
const start = Number(node.getFullStart());
|
|
245
|
+
const end = Number(node.getEnd());
|
|
246
|
+
const code = sourceFile.text.slice(start, end).trim();
|
|
247
|
+
if (!code)
|
|
248
|
+
return null;
|
|
249
|
+
return {
|
|
250
|
+
name,
|
|
251
|
+
kind: isInterface ? 'interface' : 'type',
|
|
252
|
+
file: toPosix(path.relative(root, sourceFile.fileName)),
|
|
253
|
+
code,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (DEBUG_FIELDS)
|
|
260
|
+
console.warn('[types] read error', String(error));
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
73
264
|
export const buildManifest = (input) => {
|
|
74
265
|
const { createChecker } = require('vue-component-meta');
|
|
75
266
|
const tsconfigPath = path.isAbsolute(input.tsconfig)
|
|
@@ -102,7 +293,7 @@ export const buildManifest = (input) => {
|
|
|
102
293
|
};
|
|
103
294
|
const sceneTexts = new Map();
|
|
104
295
|
for (const scene of input.scenes) {
|
|
105
|
-
//
|
|
296
|
+
// 合成条目没有源文件,rel 为空,不能去 read
|
|
106
297
|
if (!scene.rel) {
|
|
107
298
|
sceneTexts.set(scene.id, '');
|
|
108
299
|
continue;
|
|
@@ -112,73 +303,370 @@ export const buildManifest = (input) => {
|
|
|
112
303
|
}
|
|
113
304
|
const components = findComponentFiles(input.root, input.patterns).map((rel) => {
|
|
114
305
|
const meta = readMeta(rel);
|
|
306
|
+
/** 本组件契约引用到的具名类型,按名字去重 */
|
|
307
|
+
const typeRefs = new Map();
|
|
308
|
+
const collectType = (type) => {
|
|
309
|
+
const item = readTypeDeclaration(type, input.root);
|
|
310
|
+
if (item && !typeRefs.has(item.name))
|
|
311
|
+
typeRefs.set(item.name, item);
|
|
312
|
+
};
|
|
115
313
|
const props = meta.props
|
|
116
314
|
.filter((prop) => isOwnProp(prop.name))
|
|
117
|
-
.map((prop) =>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
315
|
+
.map((prop) => {
|
|
316
|
+
const type = normalizeType(prop.type);
|
|
317
|
+
const fields = readFields(prop);
|
|
318
|
+
collectType(typeObjectOf(prop));
|
|
319
|
+
return {
|
|
320
|
+
name: prop.name,
|
|
321
|
+
type,
|
|
322
|
+
required: Boolean(prop.required),
|
|
323
|
+
default: String(prop.default ?? ''),
|
|
324
|
+
description: prop.description ?? '',
|
|
325
|
+
...(fields.length ? { fields } : {}),
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
const events = meta.events.map((event) => {
|
|
329
|
+
const fields = readEventFields(event);
|
|
330
|
+
const tuple = typeObjectOf(event);
|
|
331
|
+
for (const arg of tuple?.typeArguments ?? [])
|
|
332
|
+
collectType(arg);
|
|
333
|
+
return {
|
|
334
|
+
name: event.name,
|
|
335
|
+
type: normalizeType(event.type),
|
|
336
|
+
signature: String(event.signature ?? ''),
|
|
337
|
+
description: event.description ?? '',
|
|
338
|
+
...(fields.length ? { fields } : {}),
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
const slots = meta.slots.map((slot) => {
|
|
342
|
+
collectType(typeObjectOf(slot));
|
|
343
|
+
return {
|
|
344
|
+
name: slot.name,
|
|
345
|
+
type: normalizeType(slot.type),
|
|
346
|
+
description: slot.description ?? '',
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
const exposed = meta.exposed.map((item) => {
|
|
350
|
+
collectType(typeObjectOf(item));
|
|
351
|
+
return {
|
|
352
|
+
name: item.name,
|
|
353
|
+
type: normalizeType(item.type),
|
|
354
|
+
description: item.description ?? '',
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
// 只有 .vue 场景算「用到了这个组件」:Markdown 文档里提到 prop 名字不算演示
|
|
358
|
+
const demos = input.scenes.filter((scene) => (scene.lang ?? 'vue') === 'vue');
|
|
124
359
|
// 关联场景:场景源码里出现过该组件的文件名(import 路径或组件标签)
|
|
125
360
|
const base = componentName(rel);
|
|
126
|
-
const related =
|
|
361
|
+
const related = demos.filter((scene) => {
|
|
127
362
|
const text = sceneTexts.get(scene.id) ?? '';
|
|
128
363
|
return scene.sources.some((source) => source.rel === rel) || text.includes(base);
|
|
129
364
|
});
|
|
130
365
|
const uncoveredProps = props
|
|
131
366
|
.filter((prop) => !related.some((scene) => new RegExp(`\\b${prop.name}\\b`).test(sceneTexts.get(scene.id) ?? '')))
|
|
132
367
|
.map((prop) => prop.name);
|
|
368
|
+
// 注意:vue-component-meta 不解析 defineEmits 的 JSDoc,事件说明只能靠 pageMeta.apiNotes 补,
|
|
369
|
+
// 所以这里不把 events 计为「缺说明」,否则每个组件都恒缺。
|
|
370
|
+
const undocumented = [
|
|
371
|
+
...props.filter((item) => !item.description).map((item) => `props:${item.name}`),
|
|
372
|
+
...slots.filter((item) => !item.description).map((item) => `slots:${item.name}`),
|
|
373
|
+
...exposed.filter((item) => !item.description).map((item) => `exposed:${item.name}`),
|
|
374
|
+
];
|
|
133
375
|
return {
|
|
134
376
|
name: meta.name || base,
|
|
135
377
|
rel,
|
|
136
378
|
props,
|
|
137
|
-
events
|
|
138
|
-
slots
|
|
139
|
-
exposed
|
|
379
|
+
events,
|
|
380
|
+
slots,
|
|
381
|
+
exposed,
|
|
382
|
+
...(typeRefs.size ? { types: [...typeRefs.values()] } : {}),
|
|
140
383
|
scenes: related.map((scene) => scene.id),
|
|
141
384
|
uncoveredProps,
|
|
385
|
+
undocumented,
|
|
142
386
|
};
|
|
143
387
|
});
|
|
144
388
|
return {
|
|
145
389
|
generatedAt: new Date().toISOString(),
|
|
146
390
|
components,
|
|
391
|
+
modules: input.modules ?? [],
|
|
392
|
+
env: input.env,
|
|
147
393
|
scenes: input.scenes.map((scene) => ({
|
|
148
394
|
id: scene.id,
|
|
149
395
|
title: scene.title,
|
|
150
396
|
group: scene.group,
|
|
397
|
+
lang: scene.lang ?? 'vue',
|
|
151
398
|
rel: scene.rel,
|
|
152
399
|
sources: scene.sources.map((source) => source.rel),
|
|
153
400
|
})),
|
|
154
401
|
};
|
|
155
402
|
};
|
|
156
|
-
|
|
157
|
-
const lines = [
|
|
158
|
-
|
|
159
|
-
lines.push(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
403
|
+
const renderComponentBlock = (component) => {
|
|
404
|
+
const lines = [`## ${component.name} (${component.rel})`, ''];
|
|
405
|
+
if (!component.props.length)
|
|
406
|
+
lines.push('- 无对外 props');
|
|
407
|
+
for (const prop of component.props) {
|
|
408
|
+
const flags = [prop.required ? 'required' : '', prop.default ? `default=${prop.default}` : '']
|
|
409
|
+
.filter(Boolean)
|
|
410
|
+
.join(', ');
|
|
411
|
+
lines.push(`- prop ${prop.name}: ${prop.type}${flags ? ` (${flags})` : ''}`);
|
|
412
|
+
if (prop.description)
|
|
413
|
+
lines.push(` ${prop.description}`);
|
|
414
|
+
}
|
|
415
|
+
if (component.events.length) {
|
|
416
|
+
lines.push(`- events: ${component.events.map((event) => `${event.name}${event.type}`).join(', ')}`);
|
|
417
|
+
}
|
|
418
|
+
if (component.slots.length) {
|
|
419
|
+
lines.push(`- slots: ${component.slots.map((slot) => `${slot.name}${slot.type ? ` ${slot.type}` : ''}`).join(', ')}`);
|
|
420
|
+
}
|
|
421
|
+
if (component.exposed.length) {
|
|
422
|
+
lines.push(`- exposed: ${component.exposed.map((item) => `${item.name}${item.type ? `: ${item.type}` : ''}`).join(', ')}`);
|
|
423
|
+
}
|
|
424
|
+
if (component.types?.length) {
|
|
425
|
+
lines.push(`- 类型定义: ${component.types.map((item) => `${item.name} (${item.file})`).join(', ')}`);
|
|
426
|
+
}
|
|
427
|
+
if (component.scenes.length)
|
|
428
|
+
lines.push(`- 相关场景: ${component.scenes.join(', ')}`);
|
|
429
|
+
if (component.uncoveredProps.length)
|
|
430
|
+
lines.push(`- 未覆盖 props: ${component.uncoveredProps.join(', ')}`);
|
|
431
|
+
if (component.undocumented.length)
|
|
432
|
+
lines.push(`- 缺说明: ${component.undocumented.join(', ')}`);
|
|
433
|
+
lines.push('');
|
|
434
|
+
return lines;
|
|
435
|
+
};
|
|
436
|
+
const renderModuleBlock = (module) => {
|
|
437
|
+
const lines = [`## ${module.name} (${module.rel})`, ''];
|
|
438
|
+
for (const fn of module.functions) {
|
|
439
|
+
lines.push(`- ${fn.kind} ${fn.signature}`);
|
|
440
|
+
if (fn.description)
|
|
441
|
+
lines.push(` ${fn.description}`);
|
|
442
|
+
for (const param of fn.params) {
|
|
443
|
+
const flags = [param.optional ? '可选' : '', param.default ? `默认 ${param.default}` : '']
|
|
164
444
|
.filter(Boolean)
|
|
165
445
|
.join(', ');
|
|
166
|
-
lines.push(
|
|
167
|
-
if (prop.description)
|
|
168
|
-
lines.push(` ${prop.description}`);
|
|
446
|
+
lines.push(` - 参数 ${param.name}: ${param.type}${flags ? ` (${flags})` : ''}${param.description ? ` — ${param.description}` : ''}`);
|
|
169
447
|
}
|
|
170
|
-
if (
|
|
171
|
-
lines.push(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
448
|
+
if (fn.returns)
|
|
449
|
+
lines.push(` 返回: ${fn.returns}`);
|
|
450
|
+
}
|
|
451
|
+
for (const constant of module.constants) {
|
|
452
|
+
lines.push(`- const ${constant.name}: ${constant.type}${constant.value ? ` = ${constant.value}` : ''}`);
|
|
453
|
+
if (constant.description)
|
|
454
|
+
lines.push(` ${constant.description}`);
|
|
455
|
+
}
|
|
456
|
+
if (module.types.length)
|
|
457
|
+
lines.push(`- 类型定义: ${module.types.map((item) => `${item.name} (${item.file})`).join(', ')}`);
|
|
458
|
+
if (module.scenes.length)
|
|
459
|
+
lines.push(`- 相关场景: ${module.scenes.join(', ')}`);
|
|
460
|
+
if (module.uncovered.length)
|
|
461
|
+
lines.push(`- 未覆盖: ${module.uncovered.join(', ')}`);
|
|
462
|
+
lines.push('');
|
|
463
|
+
return lines;
|
|
464
|
+
};
|
|
465
|
+
/** 环境段:AI 要先知道「这是什么站、能用什么」 */
|
|
466
|
+
const renderEnvSection = (env, manifest) => {
|
|
467
|
+
const lines = ['## 1. 环境', ''];
|
|
468
|
+
const groups = env?.groups ?? [];
|
|
469
|
+
const pages = env?.pages ?? [];
|
|
470
|
+
const globals = env?.globals ?? {};
|
|
471
|
+
const plugins = env?.plugins ?? [];
|
|
472
|
+
const sceneRoot = env?.sceneRoot ?? 'src/scenes';
|
|
473
|
+
const routeBase = env?.routeBase ?? '/';
|
|
474
|
+
const theme = env?.theme ?? { ui: 'auto', stage: 'light' };
|
|
475
|
+
lines.push('| 项 | 值 |', '|---|---|');
|
|
476
|
+
lines.push(`| 项目标题 | ${env?.title || 'rinova-playground'} |`);
|
|
477
|
+
lines.push(`| 场景目录 | \`${sceneRoot}\`(严格三层:\`<分组>/<页面>/<场景>\`,子组件页可四层) |`);
|
|
478
|
+
lines.push(`| 路由 | history 路由:\`${routeBase}\` + \`<分组>/<页面>\`,页内锚点 \`#<场景名>\` |`);
|
|
479
|
+
lines.push(`| 分组 | ${groups.length ? groups.map((group) => `\`${group.dir}\` = ${group.label}`).join(';') : '未配置 groups'} |`);
|
|
480
|
+
lines.push(`| 页面 / 场景 | ${pages.length} 个页面 / ${manifest.scenes.length} 个场景 |`);
|
|
481
|
+
lines.push('| 外壳能力 | 明暗切换、预览宽度(自适应 / 手机 390 / 平板 820 / 桌面 1280)、源码卡片内搜索、页内索引 |');
|
|
482
|
+
lines.push(`| 契约 | 组件 ${env?.components.count ?? manifest.components.length} 个${env?.components.configured === false ? '(未配置 components)' : ''};模块 ${env?.modules.count ?? manifest.modules.length} 个${env?.modules.configured === false ? '(未配置 modules)' : ''};插件 ${plugins.length} 个 |`);
|
|
483
|
+
lines.push(`| 主题 | 外壳 ${theme.ui}、演示画布 ${theme.stage};所有颜色走 \`var(--rp-color-*)\` |`);
|
|
484
|
+
lines.push('');
|
|
485
|
+
lines.push('### globals(场景里 `usePlaygroundEnv()` 读取)', '');
|
|
486
|
+
if (Object.keys(globals).length) {
|
|
487
|
+
lines.push('```json', JSON.stringify(globals, null, 2), '```', '');
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
lines.push('没有配置 `globals`。', '');
|
|
491
|
+
}
|
|
492
|
+
lines.push('### 插件', '');
|
|
493
|
+
if (!plugins.length) {
|
|
494
|
+
lines.push('没有配置 `plugins`;场景里不能假设存在登录态 / 环境变量。', '');
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
for (const plugin of plugins)
|
|
498
|
+
lines.push(`- \`${plugin}\``);
|
|
180
499
|
lines.push('');
|
|
181
500
|
}
|
|
501
|
+
lines.push('```ts');
|
|
502
|
+
lines.push("import { usePlaygroundEnv, usePlaygroundPlugin } from '@rinova/playground'");
|
|
503
|
+
lines.push('');
|
|
504
|
+
lines.push('// globals 与插件返回值按声明顺序浅合并');
|
|
505
|
+
lines.push('const env = usePlaygroundEnv<{ apiBase: string }>()');
|
|
506
|
+
lines.push("// 单个插件的返回值;name 是插件在 ctx.setup 里注册的名字");
|
|
507
|
+
lines.push("const auth = usePlaygroundPlugin<{ user: { name: string } }>('auth')");
|
|
508
|
+
lines.push('```');
|
|
509
|
+
lines.push('');
|
|
510
|
+
lines.push('插件在 `PlaygroundRoot` 初始化完成后才可用;初始化失败时整页错误卡片,`optional: true` 的插件会被跳过。');
|
|
511
|
+
lines.push('');
|
|
512
|
+
return lines;
|
|
513
|
+
};
|
|
514
|
+
/** 写作规则 + 最小骨架:AI 照这个写就能被框架认出来 */
|
|
515
|
+
const renderAuthoringSection = () => [
|
|
516
|
+
'## 2. 写作规则',
|
|
517
|
+
'',
|
|
518
|
+
'1. 目录严格三层:`<场景根>/<分组目录>/<页面目录>/<场景名>.vue`;组件有子组件时允许四层 `<页面目录>/<子页目录>/`。',
|
|
519
|
+
'2. 分组目录必须先在 `playground.config.ts` 的 `groups` 里登记,否则侧栏显示英文目录名。',
|
|
520
|
+
'3. 页面信息写在 `<页面目录>/meta.ts` 的 `pageMeta`:组件页写 `component`,模块页写 `module`,都不是就是内容页(必须写 `showSource: false`)。',
|
|
521
|
+
'4. 场景信息写在 `.vue` 的**普通 `<script>`** 块(不是 `<script setup>`)里的 `sceneMeta`;`.md` 场景写在 `---` 头里。',
|
|
522
|
+
'5. 样式只能 `<style scoped lang="less">`,禁止命中 `:root` / `html` / `body`(会污染整个 playground,verify 会判 FAIL)。',
|
|
523
|
+
'6. 只写 TypeScript、只用箭头函数;场景根不用写 `w-full`,框架已经给 `.pw-stage__inner > *` 封顶。',
|
|
524
|
+
'7. 参数与输出用 `useSceneArgs(defaults, { options, labels, slot? })` / `useSceneOutput(() => ({ ... }), slot?)`;`useScenePayload` 与 sceneMeta 里的 `payload` 已删除。',
|
|
525
|
+
'8. 颜色用 `var(--rp-color-*)`,不要写死十六进制或 `rgb()`。',
|
|
526
|
+
'',
|
|
527
|
+
'### sceneMeta 字段',
|
|
528
|
+
'',
|
|
529
|
+
'| 字段 | 类型 | 说明 |',
|
|
530
|
+
'|---|---|---|',
|
|
531
|
+
'| `title` | string | 页内索引 / 侧栏里的名字 |',
|
|
532
|
+
"| `order` | number | 页内顺序 |",
|
|
533
|
+
"| `section` | 'overview' / 'props' / 'events' / 'exposes' / 'slots' | 页内索引分组 |",
|
|
534
|
+
'| `api` | string[] | 这个场景演示的 API 名,会变成标题下的标签并可跳到 API 表 |',
|
|
535
|
+
'| `tags` | string[] | 搜索关键词 |',
|
|
536
|
+
'| `description` | string | 场景说明 |',
|
|
537
|
+
'| `hideInNav` | boolean | 不进侧栏,但可直接访问 |',
|
|
538
|
+
'| `files` | string[] | 源码卡片里额外展示的文件 |',
|
|
539
|
+
'| `showSource` | boolean | 是否给源码卡片(缺省跟随页 / 全局) |',
|
|
540
|
+
'',
|
|
541
|
+
'### pageMeta 字段(`<页面>/meta.ts`)',
|
|
542
|
+
'',
|
|
543
|
+
'| 字段 | 类型 | 说明 |',
|
|
544
|
+
'|---|---|---|',
|
|
545
|
+
'| `title` | string | 页名 |',
|
|
546
|
+
'| `order` | number | 组内顺序 |',
|
|
547
|
+
'| `description` | string | 页说明 |',
|
|
548
|
+
'| `component` | string | 这一页演示的组件名(必须出现在组件契约里) |',
|
|
549
|
+
'| `module` | string | 这一页演示的模块名(`config.modules` 里的文件名) |',
|
|
550
|
+
'| `apiNotes` | Record<string, string> | 补 API 说明,键 `events:press` / `props:size` |',
|
|
551
|
+
'| `showSource` | boolean | 页级源码开关 |',
|
|
552
|
+
'| `doc` | boolean | 文档页:不套演示画布 |',
|
|
553
|
+
'',
|
|
554
|
+
'## 3. 最小骨架',
|
|
555
|
+
'',
|
|
556
|
+
'场景文件 `src/scenes/<分组>/<页面>/<场景>.vue`:',
|
|
557
|
+
'',
|
|
558
|
+
'```vue',
|
|
559
|
+
'<script setup lang="ts">',
|
|
560
|
+
"import { useSceneArgs } from '@rinova/playground'",
|
|
561
|
+
"import MyButton from '@/lib/MyButton.vue'",
|
|
562
|
+
'',
|
|
563
|
+
"const args = useSceneArgs<{ size: 'md' | 'sm' | 'lg'; disabled: boolean }>({ size: 'md', disabled: false })",
|
|
564
|
+
'</script>',
|
|
565
|
+
'',
|
|
566
|
+
'<script lang="ts">',
|
|
567
|
+
'// 普通 <script> 块里的 sceneMeta 由插件在构建期读取',
|
|
568
|
+
'export const sceneMeta = {',
|
|
569
|
+
" title: '基础用法',",
|
|
570
|
+
" section: 'props',",
|
|
571
|
+
" api: ['size', 'disabled'],",
|
|
572
|
+
'}',
|
|
573
|
+
'</script>',
|
|
574
|
+
'',
|
|
575
|
+
'<template>',
|
|
576
|
+
' <div class="demo">',
|
|
577
|
+
' <MyButton :size="args.size" :disabled="args.disabled">按钮</MyButton>',
|
|
578
|
+
' </div>',
|
|
579
|
+
'</template>',
|
|
580
|
+
'',
|
|
581
|
+
'<style scoped lang="less">',
|
|
582
|
+
'.demo {',
|
|
583
|
+
' display: flex;',
|
|
584
|
+
' gap: 8px;',
|
|
585
|
+
'}',
|
|
586
|
+
'</style>',
|
|
587
|
+
'```',
|
|
588
|
+
'',
|
|
589
|
+
'新页面还需要 `<页面>/meta.ts`:',
|
|
590
|
+
'',
|
|
591
|
+
'```ts',
|
|
592
|
+
"import { definePageMeta } from '@rinova/playground'",
|
|
593
|
+
'',
|
|
594
|
+
'export const pageMeta = definePageMeta({',
|
|
595
|
+
" title: 'Button',",
|
|
596
|
+
' order: 1,',
|
|
597
|
+
" component: 'MyButton',",
|
|
598
|
+
'})',
|
|
599
|
+
'```',
|
|
600
|
+
'',
|
|
601
|
+
'Markdown 场景(`<页面>/<场景>.md`,默认不展示源码、不套画布):',
|
|
602
|
+
'',
|
|
603
|
+
'```md',
|
|
604
|
+
'---',
|
|
605
|
+
'title: 安装',
|
|
606
|
+
'order: 1',
|
|
607
|
+
'---',
|
|
608
|
+
'',
|
|
609
|
+
'正文……支持 `::: tip / info / warn / danger` 与 `::: steps`。',
|
|
610
|
+
'```',
|
|
611
|
+
'',
|
|
612
|
+
];
|
|
613
|
+
/** 页面清单:AI 要知道「已经有什么」,才知道该往哪加 */
|
|
614
|
+
const renderPageSection = (env, manifest) => {
|
|
615
|
+
const lines = ['## 4. 页面清单', ''];
|
|
616
|
+
const pages = env?.pages ?? [];
|
|
617
|
+
if (!pages.length)
|
|
618
|
+
return [...lines, '(页面清单不可用:清单里没有 env 段,重新跑 pnpm dev 生成)', ''];
|
|
619
|
+
lines.push('| 分组 | 页面 | id | 目录 | 绑定 | 场景 |', '|---|---|---|---|---|---|');
|
|
620
|
+
for (const page of pages) {
|
|
621
|
+
const binding = page.component ? `组件 ${page.component}` : page.module ? `模块 ${page.module}` : '内容页';
|
|
622
|
+
lines.push(`| ${page.group} | ${page.title} | \`${page.id}\` | \`${page.dir}\` | ${binding} | ${page.scenes.length} 个 |`);
|
|
623
|
+
}
|
|
624
|
+
lines.push('');
|
|
625
|
+
const emptyPages = pages.filter((page) => !page.scenes.length);
|
|
626
|
+
if (emptyPages.length) {
|
|
627
|
+
lines.push(`- 没有任何场景的页面:${emptyPages.map((page) => `\`${page.id}\``).join(', ')}`);
|
|
628
|
+
}
|
|
629
|
+
const emptyScenes = manifest.scenes.filter((scene) => !scene.rel);
|
|
630
|
+
if (emptyScenes.length)
|
|
631
|
+
lines.push(`- 没有源文件的场景条目:${emptyScenes.map((scene) => scene.id).join(', ')}`);
|
|
632
|
+
lines.push('');
|
|
633
|
+
return lines;
|
|
634
|
+
};
|
|
635
|
+
export const renderLlmsTxt = (manifest) => {
|
|
636
|
+
const lines = [
|
|
637
|
+
`# ${manifest.env?.title || 'rinova-playground'} · 演示环境说明(rinova-playground 自动生成)`,
|
|
638
|
+
'',
|
|
639
|
+
'> 由 `@rinova/playground-vite` 在 dev / build 时生成,随源码自动更新。要加一个演示,照 §3 的骨架写文件即可,不需要注册、不需要改配置。',
|
|
640
|
+
'> 查契约用 `@rinova/playground-mcp` 的 MCP 工具(`capability_search` / `capability_get` / `env_get` / `scene_scaffold` / `scene_validate` / `scenario_list` / `coverage_report`)。',
|
|
641
|
+
'',
|
|
642
|
+
...renderEnvSection(manifest.env, manifest),
|
|
643
|
+
...renderAuthoringSection(),
|
|
644
|
+
...renderPageSection(manifest.env, manifest),
|
|
645
|
+
'## 5. 组件契约',
|
|
646
|
+
'',
|
|
647
|
+
];
|
|
648
|
+
for (const component of manifest.components)
|
|
649
|
+
lines.push(...renderComponentBlock(component));
|
|
650
|
+
lines.push('## 6. 模块契约(函数 / hooks / 常量)', '');
|
|
651
|
+
if (!manifest.modules.length)
|
|
652
|
+
lines.push('没有配置 `config.modules`。', '');
|
|
653
|
+
for (const module of manifest.modules)
|
|
654
|
+
lines.push(...renderModuleBlock(module));
|
|
655
|
+
lines.push('## 7. 待办:覆盖与说明', '');
|
|
656
|
+
const uncovered = manifest.components.flatMap((component) => component.uncoveredProps.map((prop) => `- ${component.name}.${prop}(没有场景用到)`));
|
|
657
|
+
const undocumented = manifest.components.flatMap((component) => component.undocumented.map((item) => `- ${component.name} ${item}(没有 JSDoc)`));
|
|
658
|
+
const moduleUncovered = manifest.modules.flatMap((module) => module.uncovered.map((name) => `- ${module.name}.${name}(没有场景用到)`));
|
|
659
|
+
if (!uncovered.length && !undocumented.length && !moduleUncovered.length) {
|
|
660
|
+
lines.push('全部 props / 导出都有场景,且都有 JSDoc。', '');
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
if (uncovered.length)
|
|
664
|
+
lines.push('### 没有场景覆盖的 props', '', ...uncovered, '');
|
|
665
|
+
if (moduleUncovered.length)
|
|
666
|
+
lines.push('### 没有场景覆盖的模块导出', '', ...moduleUncovered, '');
|
|
667
|
+
if (undocumented.length)
|
|
668
|
+
lines.push('### 没有 JSDoc 的 API', '', ...undocumented, '');
|
|
669
|
+
}
|
|
182
670
|
return `${lines.join('\n').trim()}\n`;
|
|
183
671
|
};
|
|
184
672
|
export const renderCoverage = (manifest) => {
|
|
@@ -189,6 +677,7 @@ export const renderCoverage = (manifest) => {
|
|
|
189
677
|
})));
|
|
190
678
|
const total = rows.length;
|
|
191
679
|
const covered = rows.filter((row) => row.covered).length;
|
|
680
|
+
const missing = manifest.components.flatMap((component) => component.undocumented.map((item) => `| ${component.name} | ${item} |`));
|
|
192
681
|
const lines = [
|
|
193
682
|
'# Props 覆盖报告',
|
|
194
683
|
'',
|
|
@@ -199,6 +688,14 @@ export const renderCoverage = (manifest) => {
|
|
|
199
688
|
'|---|---|---|',
|
|
200
689
|
...rows.map((row) => `| ${row.component} | ${row.prop} | ${row.covered ? '是' : '否'} |`),
|
|
201
690
|
'',
|
|
691
|
+
'# 缺 JSDoc 的 API',
|
|
692
|
+
'',
|
|
693
|
+
'补上 `/** 说明 */` 就会出现在页面的 API 表里。',
|
|
694
|
+
'',
|
|
695
|
+
'| 组件 | API |',
|
|
696
|
+
'|---|---|',
|
|
697
|
+
...(missing.length ? missing : ['| — | 全部已写说明 |']),
|
|
698
|
+
'',
|
|
202
699
|
];
|
|
203
700
|
return lines.join('\n');
|
|
204
701
|
};
|