compmark-vue 0.1.3 → 0.2.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/cli.mjs +334 -27
- package/dist/index.d.mts +24 -1
- package/dist/index.mjs +328 -26
- package/package.json +12 -3
package/dist/cli.mjs
CHANGED
|
@@ -3,6 +3,25 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { compileScript, parse } from "@vue/compiler-sfc";
|
|
5
5
|
//#region src/parser.ts
|
|
6
|
+
function parseJSDocTags(comments) {
|
|
7
|
+
const result = { description: "" };
|
|
8
|
+
const descLines = [];
|
|
9
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
10
|
+
const c = comments[i];
|
|
11
|
+
if (c.type !== "CommentBlock") continue;
|
|
12
|
+
const lines = c.value.split("\n").map((l) => l.replace(/^\s*\*\s?/, "").trim()).filter((l) => l && !l.startsWith("/"));
|
|
13
|
+
for (const line of lines) if (line.startsWith("@deprecated")) result.deprecated = line.replace(/^@deprecated\s*/, "").trim() || true;
|
|
14
|
+
else if (line.startsWith("@since")) result.since = line.replace(/^@since\s*/, "").trim();
|
|
15
|
+
else if (line.startsWith("@example")) result.example = line.replace(/^@example\s*/, "").trim();
|
|
16
|
+
else if (line.startsWith("@see")) result.see = line.replace(/^@see\s*/, "").trim();
|
|
17
|
+
else if (line.startsWith("@default")) result.defaultOverride = line.replace(/^@default\s*/, "").trim();
|
|
18
|
+
else if (line.startsWith("@internal")) result.internal = true;
|
|
19
|
+
else if (!line.startsWith("@")) descLines.push(line);
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
result.description = descLines.join(" ");
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
6
25
|
function parseSFC(source, filename) {
|
|
7
26
|
const doc = {
|
|
8
27
|
name: filename.replace(/\.vue$/, "").split("/").pop() ?? "Unknown",
|
|
@@ -12,17 +31,60 @@ function parseSFC(source, filename) {
|
|
|
12
31
|
const { descriptor } = parse(source, { filename });
|
|
13
32
|
if (!descriptor.scriptSetup && !descriptor.script) return doc;
|
|
14
33
|
const compiled = compileScript(descriptor, { id: filename });
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
const componentJSDoc = extractComponentJSDoc(compiled.scriptSetupAst ?? compiled.scriptAst ?? []);
|
|
35
|
+
doc.description = componentJSDoc.description;
|
|
36
|
+
doc.internal = componentJSDoc.internal;
|
|
37
|
+
const setupAst = compiled.scriptSetupAst;
|
|
38
|
+
if (setupAst) {
|
|
39
|
+
const scriptSource = descriptor.scriptSetup?.content ?? compiled.content;
|
|
40
|
+
for (const stmt of setupAst) {
|
|
41
|
+
const calls = extractDefineCalls(stmt);
|
|
42
|
+
for (const { callee, args, leadingComments, typeParams, defaultsArg } of calls) if (callee === "defineProps" && args[0]?.type === "ObjectExpression") doc.props = extractProps(args[0], scriptSource);
|
|
43
|
+
else if (callee === "defineProps" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.props = extractTypeProps(typeParams.params[0], defaultsArg, scriptSource);
|
|
44
|
+
else if (callee === "defineEmits" && args[0]?.type === "ArrayExpression") doc.emits = extractEmits(args[0], leadingComments);
|
|
45
|
+
else if (callee === "defineEmits" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.emits = extractTypeEmits(typeParams.params[0]);
|
|
46
|
+
else if (callee === "defineSlots" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.slots = extractTypeSlots(typeParams.params[0]);
|
|
47
|
+
else if (callee === "defineExpose" && args[0]?.type === "ObjectExpression") doc.exposes = extractExposes(args[0], scriptSource);
|
|
48
|
+
}
|
|
49
|
+
doc.composables = extractComposables(setupAst);
|
|
50
|
+
}
|
|
51
|
+
const scriptAst = compiled.scriptAst;
|
|
52
|
+
if (scriptAst && doc.props.length === 0 && doc.emits.length === 0) {
|
|
53
|
+
const optionsDoc = extractOptionsAPI(scriptAst, compiled.content);
|
|
54
|
+
doc.props = optionsDoc.props;
|
|
55
|
+
doc.emits = optionsDoc.emits;
|
|
56
|
+
}
|
|
57
|
+
if (descriptor.template?.ast) {
|
|
58
|
+
const templateSlots = extractTemplateSlots(descriptor.template.ast);
|
|
59
|
+
doc.slots = mergeSlots(doc.slots ?? [], templateSlots);
|
|
23
60
|
}
|
|
24
61
|
return doc;
|
|
25
62
|
}
|
|
63
|
+
function extractComponentJSDoc(ast) {
|
|
64
|
+
if (ast.length === 0) return {
|
|
65
|
+
description: "",
|
|
66
|
+
internal: false
|
|
67
|
+
};
|
|
68
|
+
const comments = ast[0].leadingComments ?? [];
|
|
69
|
+
if (comments.length === 0) return {
|
|
70
|
+
description: "",
|
|
71
|
+
internal: false
|
|
72
|
+
};
|
|
73
|
+
const firstComment = comments[0];
|
|
74
|
+
if (firstComment.type !== "CommentBlock") return {
|
|
75
|
+
description: "",
|
|
76
|
+
internal: false
|
|
77
|
+
};
|
|
78
|
+
const result = parseJSDocTags([firstComment]);
|
|
79
|
+
if (result.internal) return {
|
|
80
|
+
description: result.description,
|
|
81
|
+
internal: true
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
description: "",
|
|
85
|
+
internal: false
|
|
86
|
+
};
|
|
87
|
+
}
|
|
26
88
|
function processCallExpression(callExpr, leadingComments) {
|
|
27
89
|
if (callExpr.callee.type === "Identifier" && callExpr.callee.name === "withDefaults" && callExpr.arguments[0]?.type === "CallExpression" && callExpr.arguments[0].callee.type === "Identifier" && callExpr.arguments[0].callee.name === "defineProps") {
|
|
28
90
|
const innerCall = callExpr.arguments[0];
|
|
@@ -58,13 +120,17 @@ function extractTypeProps(typeLiteral, defaultsArg, source) {
|
|
|
58
120
|
const name = member.key.type === "Identifier" ? member.key.name : member.key.type === "StringLiteral" ? member.key.value : "";
|
|
59
121
|
if (!name) continue;
|
|
60
122
|
const type = member.typeAnnotation?.typeAnnotation ? resolveTypeString(member.typeAnnotation.typeAnnotation) : "unknown";
|
|
61
|
-
const
|
|
123
|
+
const jsdoc = parseJSDocTags(member.leadingComments ?? []);
|
|
62
124
|
props.push({
|
|
63
125
|
name,
|
|
64
126
|
type,
|
|
65
127
|
required: !member.optional,
|
|
66
128
|
default: defaults.get(name),
|
|
67
|
-
description
|
|
129
|
+
description: jsdoc.description,
|
|
130
|
+
...jsdoc.deprecated !== void 0 && { deprecated: jsdoc.deprecated },
|
|
131
|
+
...jsdoc.since && { since: jsdoc.since },
|
|
132
|
+
...jsdoc.example && { example: jsdoc.example },
|
|
133
|
+
...jsdoc.see && { see: jsdoc.see }
|
|
68
134
|
});
|
|
69
135
|
}
|
|
70
136
|
return props;
|
|
@@ -76,6 +142,9 @@ function resolveTypeString(node) {
|
|
|
76
142
|
case "TSBooleanKeyword": return "boolean";
|
|
77
143
|
case "TSObjectKeyword": return "object";
|
|
78
144
|
case "TSAnyKeyword": return "any";
|
|
145
|
+
case "TSVoidKeyword": return "void";
|
|
146
|
+
case "TSNullKeyword": return "null";
|
|
147
|
+
case "TSUndefinedKeyword": return "undefined";
|
|
79
148
|
case "TSUnionType": return node.types.map((t) => resolveTypeString(t)).join(" | ");
|
|
80
149
|
case "TSIntersectionType": return node.types.map((t) => resolveTypeString(t)).join(" & ");
|
|
81
150
|
case "TSLiteralType":
|
|
@@ -90,6 +159,9 @@ function resolveTypeString(node) {
|
|
|
90
159
|
return name;
|
|
91
160
|
}
|
|
92
161
|
case "TSFunctionType": return "Function";
|
|
162
|
+
case "TSTupleType": return `[${node.elementTypes.map((t) => resolveTypeString(t)).join(", ")}]`;
|
|
163
|
+
case "TSNamedTupleMember": return resolveTypeString(node.elementType);
|
|
164
|
+
case "TSParenthesizedType": return resolveTypeString(node.typeAnnotation);
|
|
93
165
|
default: return "unknown";
|
|
94
166
|
}
|
|
95
167
|
}
|
|
@@ -110,13 +182,20 @@ function extractProps(obj, source) {
|
|
|
110
182
|
const p = prop;
|
|
111
183
|
const name = p.key.type === "Identifier" ? p.key.name : p.key.type === "StringLiteral" ? p.key.value : "";
|
|
112
184
|
if (!name) continue;
|
|
113
|
-
const
|
|
185
|
+
const jsdoc = parseJSDocTags(p.leadingComments ?? []);
|
|
186
|
+
const tagFields = {
|
|
187
|
+
...jsdoc.deprecated !== void 0 && { deprecated: jsdoc.deprecated },
|
|
188
|
+
...jsdoc.since && { since: jsdoc.since },
|
|
189
|
+
...jsdoc.example && { example: jsdoc.example },
|
|
190
|
+
...jsdoc.see && { see: jsdoc.see }
|
|
191
|
+
};
|
|
114
192
|
if (p.value.type === "Identifier") props.push({
|
|
115
193
|
name,
|
|
116
194
|
type: p.value.name,
|
|
117
195
|
required: false,
|
|
118
196
|
default: void 0,
|
|
119
|
-
description
|
|
197
|
+
description: jsdoc.description,
|
|
198
|
+
...tagFields
|
|
120
199
|
});
|
|
121
200
|
else if (p.value.type === "ArrayExpression") {
|
|
122
201
|
const types = p.value.elements.filter((el) => el?.type === "Identifier").map((el) => el.name);
|
|
@@ -125,7 +204,8 @@ function extractProps(obj, source) {
|
|
|
125
204
|
type: types.join(" | "),
|
|
126
205
|
required: false,
|
|
127
206
|
default: void 0,
|
|
128
|
-
description
|
|
207
|
+
description: jsdoc.description,
|
|
208
|
+
...tagFields
|
|
129
209
|
});
|
|
130
210
|
} else if (p.value.type === "ObjectExpression") {
|
|
131
211
|
let type = "unknown";
|
|
@@ -145,7 +225,8 @@ function extractProps(obj, source) {
|
|
|
145
225
|
type,
|
|
146
226
|
required,
|
|
147
227
|
default: defaultVal,
|
|
148
|
-
description
|
|
228
|
+
description: jsdoc.description,
|
|
229
|
+
...tagFields
|
|
149
230
|
});
|
|
150
231
|
}
|
|
151
232
|
}
|
|
@@ -172,14 +253,188 @@ function parseEmitJSDoc(comments) {
|
|
|
172
253
|
}
|
|
173
254
|
return map;
|
|
174
255
|
}
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
256
|
+
function extractTypeEmits(typeLiteral) {
|
|
257
|
+
const emits = [];
|
|
258
|
+
for (const member of typeLiteral.members) if (member.type === "TSPropertySignature") {
|
|
259
|
+
const name = member.key.type === "Identifier" ? member.key.name : member.key.type === "StringLiteral" ? member.key.value : "";
|
|
260
|
+
if (!name) continue;
|
|
261
|
+
const jsdoc = parseJSDocTags(member.leadingComments ?? []);
|
|
262
|
+
let payload;
|
|
263
|
+
const typeAnnotation = member.typeAnnotation?.typeAnnotation;
|
|
264
|
+
if (typeAnnotation?.type === "TSTupleType") payload = typeAnnotation.elementTypes.map((el) => {
|
|
265
|
+
if (el.type === "TSNamedTupleMember") return `${el.label?.name ?? "arg"}: ${resolveTypeString(el.elementType)}`;
|
|
266
|
+
return resolveTypeString(el);
|
|
267
|
+
}).join(", ");
|
|
268
|
+
emits.push({
|
|
269
|
+
name,
|
|
270
|
+
description: jsdoc.description,
|
|
271
|
+
...payload && { payload }
|
|
272
|
+
});
|
|
273
|
+
} else if (member.type === "TSCallSignatureDeclaration") {
|
|
274
|
+
const params = member.parameters ?? [];
|
|
275
|
+
if (params.length === 0) continue;
|
|
276
|
+
const firstParam = params[0];
|
|
277
|
+
if (firstParam?.typeAnnotation?.typeAnnotation?.type !== "TSLiteralType" || firstParam.typeAnnotation.typeAnnotation.literal.type !== "StringLiteral") continue;
|
|
278
|
+
const name = firstParam.typeAnnotation.typeAnnotation.literal.value;
|
|
279
|
+
const jsdoc = parseJSDocTags(member.leadingComments ?? []);
|
|
280
|
+
const payloadParams = params.slice(1);
|
|
281
|
+
let payload;
|
|
282
|
+
if (payloadParams.length > 0) payload = payloadParams.map((p) => {
|
|
283
|
+
return `${p.type === "Identifier" ? p.name : "arg"}: ${p.typeAnnotation?.typeAnnotation ? resolveTypeString(p.typeAnnotation.typeAnnotation) : "unknown"}`;
|
|
284
|
+
}).join(", ");
|
|
285
|
+
emits.push({
|
|
286
|
+
name,
|
|
287
|
+
description: jsdoc.description,
|
|
288
|
+
...payload && { payload }
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return emits;
|
|
292
|
+
}
|
|
293
|
+
function extractTypeSlots(typeLiteral) {
|
|
294
|
+
const slots = [];
|
|
295
|
+
for (const member of typeLiteral.members) {
|
|
296
|
+
const name = member.key?.type === "Identifier" ? member.key.name : member.key?.type === "StringLiteral" ? member.key.value : "";
|
|
297
|
+
if (!name) continue;
|
|
298
|
+
const jsdoc = parseJSDocTags(member.leadingComments ?? []);
|
|
299
|
+
const bindings = [];
|
|
300
|
+
if (member.type === "TSMethodSignature" && member.parameters?.length > 0) {
|
|
301
|
+
const propsType = member.parameters[0]?.typeAnnotation?.typeAnnotation;
|
|
302
|
+
if (propsType?.type === "TSTypeLiteral") {
|
|
303
|
+
for (const prop of propsType.members) if (prop.type === "TSPropertySignature") {
|
|
304
|
+
const propName = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : "";
|
|
305
|
+
const propType = prop.typeAnnotation?.typeAnnotation ? resolveTypeString(prop.typeAnnotation.typeAnnotation) : "unknown";
|
|
306
|
+
if (propName) bindings.push(`${propName}: ${propType}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
slots.push({
|
|
311
|
+
name,
|
|
312
|
+
description: jsdoc.description,
|
|
313
|
+
bindings
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return slots;
|
|
317
|
+
}
|
|
318
|
+
function extractExposes(obj, _source) {
|
|
319
|
+
const exposes = [];
|
|
320
|
+
for (const prop of obj.properties) {
|
|
321
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "ObjectMethod") continue;
|
|
322
|
+
const key = prop.key;
|
|
323
|
+
const name = key.type === "Identifier" ? key.name : key.type === "StringLiteral" ? key.value : "";
|
|
324
|
+
if (!name) continue;
|
|
325
|
+
const jsdoc = parseJSDocTags(prop.leadingComments ?? []);
|
|
326
|
+
exposes.push({
|
|
327
|
+
name,
|
|
328
|
+
type: "unknown",
|
|
329
|
+
description: jsdoc.description
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return exposes;
|
|
333
|
+
}
|
|
334
|
+
function extractComposables(ast) {
|
|
335
|
+
const seen = /* @__PURE__ */ new Set();
|
|
336
|
+
const composables = [];
|
|
337
|
+
for (const stmt of ast) {
|
|
338
|
+
const callNames = extractComposableCallNames(stmt);
|
|
339
|
+
for (const name of callNames) if (!seen.has(name)) {
|
|
340
|
+
seen.add(name);
|
|
341
|
+
composables.push({ name });
|
|
342
|
+
}
|
|
181
343
|
}
|
|
182
|
-
return
|
|
344
|
+
return composables;
|
|
345
|
+
}
|
|
346
|
+
function extractComposableCallNames(stmt) {
|
|
347
|
+
const names = [];
|
|
348
|
+
if (stmt.type === "ExpressionStatement" && stmt.expression.type === "CallExpression" && stmt.expression.callee.type === "Identifier" && /^use[A-Z]/.test(stmt.expression.callee.name)) names.push(stmt.expression.callee.name);
|
|
349
|
+
if (stmt.type === "VariableDeclaration") {
|
|
350
|
+
for (const decl of stmt.declarations) if (decl.init?.type === "CallExpression" && decl.init.callee.type === "Identifier" && /^use[A-Z]/.test(decl.init.callee.name)) names.push(decl.init.callee.name);
|
|
351
|
+
}
|
|
352
|
+
return names;
|
|
353
|
+
}
|
|
354
|
+
function extractTemplateSlots(templateAst) {
|
|
355
|
+
const slots = [];
|
|
356
|
+
walkTemplate(templateAst.children ?? [], slots);
|
|
357
|
+
return slots;
|
|
358
|
+
}
|
|
359
|
+
function walkTemplate(children, slots) {
|
|
360
|
+
for (const node of children) {
|
|
361
|
+
if (node.type === 1 && node.tag === "slot") {
|
|
362
|
+
let name = "default";
|
|
363
|
+
const bindings = [];
|
|
364
|
+
for (const prop of node.props ?? []) {
|
|
365
|
+
if (prop.type === 6 && prop.name === "name" && prop.value?.content) name = prop.value.content;
|
|
366
|
+
if (prop.type === 7 && prop.name === "bind" && prop.arg?.content) bindings.push(prop.arg.content);
|
|
367
|
+
}
|
|
368
|
+
slots.push({
|
|
369
|
+
name,
|
|
370
|
+
description: "",
|
|
371
|
+
bindings
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (node.children) walkTemplate(node.children, slots);
|
|
375
|
+
if (node.branches) {
|
|
376
|
+
for (const branch of node.branches) if (branch.children) walkTemplate(branch.children, slots);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function mergeSlots(typedSlots, templateSlots) {
|
|
381
|
+
const merged = [...typedSlots];
|
|
382
|
+
const typedNames = new Set(typedSlots.map((s) => s.name));
|
|
383
|
+
for (const ts of templateSlots) if (!typedNames.has(ts.name)) merged.push(ts);
|
|
384
|
+
return merged;
|
|
385
|
+
}
|
|
386
|
+
function extractOptionsAPI(ast, source) {
|
|
387
|
+
let props = [];
|
|
388
|
+
let emits = [];
|
|
389
|
+
for (const stmt of ast) {
|
|
390
|
+
if (stmt.type !== "ExportDefaultDeclaration") continue;
|
|
391
|
+
const decl = stmt.declaration;
|
|
392
|
+
if (decl.type !== "ObjectExpression") continue;
|
|
393
|
+
for (const prop of decl.properties) {
|
|
394
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
395
|
+
const key = prop.key;
|
|
396
|
+
const name = key.type === "Identifier" ? key.name : "";
|
|
397
|
+
if (name === "props") {
|
|
398
|
+
if (prop.value.type === "ObjectExpression") props = extractProps(prop.value, source);
|
|
399
|
+
else if (prop.value.type === "ArrayExpression") props = extractArrayProps(prop.value);
|
|
400
|
+
} else if (name === "emits") {
|
|
401
|
+
if (prop.value.type === "ArrayExpression") {
|
|
402
|
+
const comments = stmt.leadingComments ?? [];
|
|
403
|
+
emits = extractEmits(prop.value, comments);
|
|
404
|
+
} else if (prop.value.type === "ObjectExpression") emits = extractObjectEmits(prop.value);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
props,
|
|
411
|
+
emits
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function extractArrayProps(arr) {
|
|
415
|
+
const props = [];
|
|
416
|
+
for (const el of arr.elements) if (el?.type === "StringLiteral") props.push({
|
|
417
|
+
name: el.value,
|
|
418
|
+
type: "unknown",
|
|
419
|
+
required: false,
|
|
420
|
+
default: void 0,
|
|
421
|
+
description: ""
|
|
422
|
+
});
|
|
423
|
+
return props;
|
|
424
|
+
}
|
|
425
|
+
function extractObjectEmits(obj) {
|
|
426
|
+
const emits = [];
|
|
427
|
+
for (const prop of obj.properties) {
|
|
428
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
429
|
+
const name = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : "";
|
|
430
|
+
if (!name) continue;
|
|
431
|
+
const jsdoc = parseJSDocTags(prop.leadingComments ?? []);
|
|
432
|
+
emits.push({
|
|
433
|
+
name,
|
|
434
|
+
description: jsdoc.description
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return emits;
|
|
183
438
|
}
|
|
184
439
|
function stringifyDefault(node, source) {
|
|
185
440
|
switch (node.type) {
|
|
@@ -198,22 +453,46 @@ function stringifyDefault(node, source) {
|
|
|
198
453
|
//#region src/markdown.ts
|
|
199
454
|
function generateMarkdown(doc) {
|
|
200
455
|
const sections = [`# ${doc.name}`];
|
|
201
|
-
if (doc.
|
|
202
|
-
|
|
456
|
+
if (doc.description) sections.push("", doc.description);
|
|
457
|
+
const hasProps = doc.props.length > 0;
|
|
458
|
+
const hasEmits = doc.emits.length > 0;
|
|
459
|
+
const hasSlots = (doc.slots?.length ?? 0) > 0;
|
|
460
|
+
const hasExposes = (doc.exposes?.length ?? 0) > 0;
|
|
461
|
+
const hasComposables = (doc.composables?.length ?? 0) > 0;
|
|
462
|
+
if (!hasProps && !hasEmits && !hasSlots && !hasExposes && !hasComposables) {
|
|
463
|
+
sections.push("", "No documentable API found.");
|
|
203
464
|
return sections.join("\n") + "\n";
|
|
204
465
|
}
|
|
205
|
-
if (
|
|
466
|
+
if (hasProps) {
|
|
206
467
|
sections.push("", "## Props", "");
|
|
207
468
|
sections.push("| Name | Type | Required | Default | Description |");
|
|
208
469
|
sections.push("| --- | --- | --- | --- | --- |");
|
|
470
|
+
const examples = [];
|
|
209
471
|
for (const p of doc.props) {
|
|
210
472
|
const def = p.default !== void 0 ? `\`${p.default}\`` : "-";
|
|
211
|
-
|
|
473
|
+
let desc = p.description || "-";
|
|
474
|
+
if (p.deprecated) desc += typeof p.deprecated === "string" && p.deprecated ? ` **Deprecated**: ${p.deprecated}` : " **Deprecated**";
|
|
475
|
+
if (p.since) desc += ` *(since ${p.since})*`;
|
|
476
|
+
if (p.see) desc += ` See: ${p.see}`;
|
|
212
477
|
const req = p.required ? "Yes" : "No";
|
|
213
478
|
sections.push(`| ${p.name} | ${p.type} | ${req} | ${def} | ${desc} |`);
|
|
479
|
+
if (p.example) examples.push({
|
|
480
|
+
name: p.name,
|
|
481
|
+
example: p.example
|
|
482
|
+
});
|
|
214
483
|
}
|
|
484
|
+
for (const { name, example } of examples) sections.push("", `**\`${name}\` example:**`, "", "```", example, "```");
|
|
215
485
|
}
|
|
216
|
-
if (doc.emits.
|
|
486
|
+
if (hasEmits) if (doc.emits.some((e) => e.payload)) {
|
|
487
|
+
sections.push("", "## Emits", "");
|
|
488
|
+
sections.push("| Name | Payload | Description |");
|
|
489
|
+
sections.push("| --- | --- | --- |");
|
|
490
|
+
for (const e of doc.emits) {
|
|
491
|
+
const desc = e.description || "-";
|
|
492
|
+
const payload = e.payload || "-";
|
|
493
|
+
sections.push(`| ${e.name} | ${payload} | ${desc} |`);
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
217
496
|
sections.push("", "## Emits", "");
|
|
218
497
|
sections.push("| Name | Description |");
|
|
219
498
|
sections.push("| --- | --- |");
|
|
@@ -222,6 +501,29 @@ function generateMarkdown(doc) {
|
|
|
222
501
|
sections.push(`| ${e.name} | ${desc} |`);
|
|
223
502
|
}
|
|
224
503
|
}
|
|
504
|
+
if (hasSlots) {
|
|
505
|
+
sections.push("", "## Slots", "");
|
|
506
|
+
sections.push("| Name | Bindings | Description |");
|
|
507
|
+
sections.push("| --- | --- | --- |");
|
|
508
|
+
for (const s of doc.slots) {
|
|
509
|
+
const desc = s.description || "-";
|
|
510
|
+
const bindings = s.bindings.length > 0 ? s.bindings.join(", ") : "-";
|
|
511
|
+
sections.push(`| ${s.name} | ${bindings} | ${desc} |`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (hasExposes) {
|
|
515
|
+
sections.push("", "## Exposed", "");
|
|
516
|
+
sections.push("| Name | Type | Description |");
|
|
517
|
+
sections.push("| --- | --- | --- |");
|
|
518
|
+
for (const e of doc.exposes) {
|
|
519
|
+
const desc = e.description || "-";
|
|
520
|
+
sections.push(`| ${e.name} | ${e.type} | ${desc} |`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (hasComposables) {
|
|
524
|
+
sections.push("", "## Composables Used", "");
|
|
525
|
+
for (const c of doc.composables) sections.push(`- \`${c.name}\``);
|
|
526
|
+
}
|
|
225
527
|
return sections.join("\n") + "\n";
|
|
226
528
|
}
|
|
227
529
|
//#endregion
|
|
@@ -234,7 +536,7 @@ function parseComponent(filePath) {
|
|
|
234
536
|
//#region src/cli.ts
|
|
235
537
|
const filePath = process.argv[2];
|
|
236
538
|
if (!filePath) {
|
|
237
|
-
console.error("Usage: compmark
|
|
539
|
+
console.error("Usage: compmark <path-to-component.vue>");
|
|
238
540
|
process.exit(1);
|
|
239
541
|
}
|
|
240
542
|
if (!filePath.endsWith(".vue")) {
|
|
@@ -248,6 +550,11 @@ if (!existsSync(abs)) {
|
|
|
248
550
|
}
|
|
249
551
|
try {
|
|
250
552
|
const doc = parseComponent(abs);
|
|
553
|
+
if (doc.internal) {
|
|
554
|
+
const name = abs.split("/").pop() ?? filePath;
|
|
555
|
+
console.log(`Skipped ${name} (marked @internal)`);
|
|
556
|
+
process.exit(0);
|
|
557
|
+
}
|
|
251
558
|
const md = generateMarkdown(doc);
|
|
252
559
|
const outFile = `${doc.name}.md`;
|
|
253
560
|
writeFileSync(join(process.cwd(), outFile), md, "utf-8");
|
package/dist/index.d.mts
CHANGED
|
@@ -5,15 +5,38 @@ interface PropDoc {
|
|
|
5
5
|
required: boolean;
|
|
6
6
|
default: string | undefined;
|
|
7
7
|
description: string;
|
|
8
|
+
deprecated?: string | boolean;
|
|
9
|
+
since?: string;
|
|
10
|
+
example?: string;
|
|
11
|
+
see?: string;
|
|
8
12
|
}
|
|
9
13
|
interface EmitDoc {
|
|
10
14
|
name: string;
|
|
11
15
|
description: string;
|
|
16
|
+
payload?: string;
|
|
17
|
+
}
|
|
18
|
+
interface SlotDoc {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
bindings: string[];
|
|
22
|
+
}
|
|
23
|
+
interface ExposeDoc {
|
|
24
|
+
name: string;
|
|
25
|
+
type: string;
|
|
26
|
+
description: string;
|
|
27
|
+
}
|
|
28
|
+
interface ComposableDoc {
|
|
29
|
+
name: string;
|
|
12
30
|
}
|
|
13
31
|
interface ComponentDoc {
|
|
14
32
|
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
internal?: boolean;
|
|
15
35
|
props: PropDoc[];
|
|
16
36
|
emits: EmitDoc[];
|
|
37
|
+
slots?: SlotDoc[];
|
|
38
|
+
exposes?: ExposeDoc[];
|
|
39
|
+
composables?: ComposableDoc[];
|
|
17
40
|
}
|
|
18
41
|
//#endregion
|
|
19
42
|
//#region src/parser.d.ts
|
|
@@ -25,4 +48,4 @@ declare function generateMarkdown(doc: ComponentDoc): string;
|
|
|
25
48
|
//#region src/index.d.ts
|
|
26
49
|
declare function parseComponent(filePath: string): ComponentDoc;
|
|
27
50
|
//#endregion
|
|
28
|
-
export { type ComponentDoc, type EmitDoc, type PropDoc, generateMarkdown, parseComponent, parseSFC };
|
|
51
|
+
export { type ComponentDoc, type ComposableDoc, type EmitDoc, type ExposeDoc, type PropDoc, type SlotDoc, generateMarkdown, parseComponent, parseSFC };
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,25 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { compileScript, parse } from "@vue/compiler-sfc";
|
|
4
4
|
//#region src/parser.ts
|
|
5
|
+
function parseJSDocTags(comments) {
|
|
6
|
+
const result = { description: "" };
|
|
7
|
+
const descLines = [];
|
|
8
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
9
|
+
const c = comments[i];
|
|
10
|
+
if (c.type !== "CommentBlock") continue;
|
|
11
|
+
const lines = c.value.split("\n").map((l) => l.replace(/^\s*\*\s?/, "").trim()).filter((l) => l && !l.startsWith("/"));
|
|
12
|
+
for (const line of lines) if (line.startsWith("@deprecated")) result.deprecated = line.replace(/^@deprecated\s*/, "").trim() || true;
|
|
13
|
+
else if (line.startsWith("@since")) result.since = line.replace(/^@since\s*/, "").trim();
|
|
14
|
+
else if (line.startsWith("@example")) result.example = line.replace(/^@example\s*/, "").trim();
|
|
15
|
+
else if (line.startsWith("@see")) result.see = line.replace(/^@see\s*/, "").trim();
|
|
16
|
+
else if (line.startsWith("@default")) result.defaultOverride = line.replace(/^@default\s*/, "").trim();
|
|
17
|
+
else if (line.startsWith("@internal")) result.internal = true;
|
|
18
|
+
else if (!line.startsWith("@")) descLines.push(line);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
result.description = descLines.join(" ");
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
5
24
|
function parseSFC(source, filename) {
|
|
6
25
|
const doc = {
|
|
7
26
|
name: filename.replace(/\.vue$/, "").split("/").pop() ?? "Unknown",
|
|
@@ -11,17 +30,60 @@ function parseSFC(source, filename) {
|
|
|
11
30
|
const { descriptor } = parse(source, { filename });
|
|
12
31
|
if (!descriptor.scriptSetup && !descriptor.script) return doc;
|
|
13
32
|
const compiled = compileScript(descriptor, { id: filename });
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
const componentJSDoc = extractComponentJSDoc(compiled.scriptSetupAst ?? compiled.scriptAst ?? []);
|
|
34
|
+
doc.description = componentJSDoc.description;
|
|
35
|
+
doc.internal = componentJSDoc.internal;
|
|
36
|
+
const setupAst = compiled.scriptSetupAst;
|
|
37
|
+
if (setupAst) {
|
|
38
|
+
const scriptSource = descriptor.scriptSetup?.content ?? compiled.content;
|
|
39
|
+
for (const stmt of setupAst) {
|
|
40
|
+
const calls = extractDefineCalls(stmt);
|
|
41
|
+
for (const { callee, args, leadingComments, typeParams, defaultsArg } of calls) if (callee === "defineProps" && args[0]?.type === "ObjectExpression") doc.props = extractProps(args[0], scriptSource);
|
|
42
|
+
else if (callee === "defineProps" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.props = extractTypeProps(typeParams.params[0], defaultsArg, scriptSource);
|
|
43
|
+
else if (callee === "defineEmits" && args[0]?.type === "ArrayExpression") doc.emits = extractEmits(args[0], leadingComments);
|
|
44
|
+
else if (callee === "defineEmits" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.emits = extractTypeEmits(typeParams.params[0]);
|
|
45
|
+
else if (callee === "defineSlots" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.slots = extractTypeSlots(typeParams.params[0]);
|
|
46
|
+
else if (callee === "defineExpose" && args[0]?.type === "ObjectExpression") doc.exposes = extractExposes(args[0], scriptSource);
|
|
47
|
+
}
|
|
48
|
+
doc.composables = extractComposables(setupAst);
|
|
49
|
+
}
|
|
50
|
+
const scriptAst = compiled.scriptAst;
|
|
51
|
+
if (scriptAst && doc.props.length === 0 && doc.emits.length === 0) {
|
|
52
|
+
const optionsDoc = extractOptionsAPI(scriptAst, compiled.content);
|
|
53
|
+
doc.props = optionsDoc.props;
|
|
54
|
+
doc.emits = optionsDoc.emits;
|
|
55
|
+
}
|
|
56
|
+
if (descriptor.template?.ast) {
|
|
57
|
+
const templateSlots = extractTemplateSlots(descriptor.template.ast);
|
|
58
|
+
doc.slots = mergeSlots(doc.slots ?? [], templateSlots);
|
|
22
59
|
}
|
|
23
60
|
return doc;
|
|
24
61
|
}
|
|
62
|
+
function extractComponentJSDoc(ast) {
|
|
63
|
+
if (ast.length === 0) return {
|
|
64
|
+
description: "",
|
|
65
|
+
internal: false
|
|
66
|
+
};
|
|
67
|
+
const comments = ast[0].leadingComments ?? [];
|
|
68
|
+
if (comments.length === 0) return {
|
|
69
|
+
description: "",
|
|
70
|
+
internal: false
|
|
71
|
+
};
|
|
72
|
+
const firstComment = comments[0];
|
|
73
|
+
if (firstComment.type !== "CommentBlock") return {
|
|
74
|
+
description: "",
|
|
75
|
+
internal: false
|
|
76
|
+
};
|
|
77
|
+
const result = parseJSDocTags([firstComment]);
|
|
78
|
+
if (result.internal) return {
|
|
79
|
+
description: result.description,
|
|
80
|
+
internal: true
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
description: "",
|
|
84
|
+
internal: false
|
|
85
|
+
};
|
|
86
|
+
}
|
|
25
87
|
function processCallExpression(callExpr, leadingComments) {
|
|
26
88
|
if (callExpr.callee.type === "Identifier" && callExpr.callee.name === "withDefaults" && callExpr.arguments[0]?.type === "CallExpression" && callExpr.arguments[0].callee.type === "Identifier" && callExpr.arguments[0].callee.name === "defineProps") {
|
|
27
89
|
const innerCall = callExpr.arguments[0];
|
|
@@ -57,13 +119,17 @@ function extractTypeProps(typeLiteral, defaultsArg, source) {
|
|
|
57
119
|
const name = member.key.type === "Identifier" ? member.key.name : member.key.type === "StringLiteral" ? member.key.value : "";
|
|
58
120
|
if (!name) continue;
|
|
59
121
|
const type = member.typeAnnotation?.typeAnnotation ? resolveTypeString(member.typeAnnotation.typeAnnotation) : "unknown";
|
|
60
|
-
const
|
|
122
|
+
const jsdoc = parseJSDocTags(member.leadingComments ?? []);
|
|
61
123
|
props.push({
|
|
62
124
|
name,
|
|
63
125
|
type,
|
|
64
126
|
required: !member.optional,
|
|
65
127
|
default: defaults.get(name),
|
|
66
|
-
description
|
|
128
|
+
description: jsdoc.description,
|
|
129
|
+
...jsdoc.deprecated !== void 0 && { deprecated: jsdoc.deprecated },
|
|
130
|
+
...jsdoc.since && { since: jsdoc.since },
|
|
131
|
+
...jsdoc.example && { example: jsdoc.example },
|
|
132
|
+
...jsdoc.see && { see: jsdoc.see }
|
|
67
133
|
});
|
|
68
134
|
}
|
|
69
135
|
return props;
|
|
@@ -75,6 +141,9 @@ function resolveTypeString(node) {
|
|
|
75
141
|
case "TSBooleanKeyword": return "boolean";
|
|
76
142
|
case "TSObjectKeyword": return "object";
|
|
77
143
|
case "TSAnyKeyword": return "any";
|
|
144
|
+
case "TSVoidKeyword": return "void";
|
|
145
|
+
case "TSNullKeyword": return "null";
|
|
146
|
+
case "TSUndefinedKeyword": return "undefined";
|
|
78
147
|
case "TSUnionType": return node.types.map((t) => resolveTypeString(t)).join(" | ");
|
|
79
148
|
case "TSIntersectionType": return node.types.map((t) => resolveTypeString(t)).join(" & ");
|
|
80
149
|
case "TSLiteralType":
|
|
@@ -89,6 +158,9 @@ function resolveTypeString(node) {
|
|
|
89
158
|
return name;
|
|
90
159
|
}
|
|
91
160
|
case "TSFunctionType": return "Function";
|
|
161
|
+
case "TSTupleType": return `[${node.elementTypes.map((t) => resolveTypeString(t)).join(", ")}]`;
|
|
162
|
+
case "TSNamedTupleMember": return resolveTypeString(node.elementType);
|
|
163
|
+
case "TSParenthesizedType": return resolveTypeString(node.typeAnnotation);
|
|
92
164
|
default: return "unknown";
|
|
93
165
|
}
|
|
94
166
|
}
|
|
@@ -109,13 +181,20 @@ function extractProps(obj, source) {
|
|
|
109
181
|
const p = prop;
|
|
110
182
|
const name = p.key.type === "Identifier" ? p.key.name : p.key.type === "StringLiteral" ? p.key.value : "";
|
|
111
183
|
if (!name) continue;
|
|
112
|
-
const
|
|
184
|
+
const jsdoc = parseJSDocTags(p.leadingComments ?? []);
|
|
185
|
+
const tagFields = {
|
|
186
|
+
...jsdoc.deprecated !== void 0 && { deprecated: jsdoc.deprecated },
|
|
187
|
+
...jsdoc.since && { since: jsdoc.since },
|
|
188
|
+
...jsdoc.example && { example: jsdoc.example },
|
|
189
|
+
...jsdoc.see && { see: jsdoc.see }
|
|
190
|
+
};
|
|
113
191
|
if (p.value.type === "Identifier") props.push({
|
|
114
192
|
name,
|
|
115
193
|
type: p.value.name,
|
|
116
194
|
required: false,
|
|
117
195
|
default: void 0,
|
|
118
|
-
description
|
|
196
|
+
description: jsdoc.description,
|
|
197
|
+
...tagFields
|
|
119
198
|
});
|
|
120
199
|
else if (p.value.type === "ArrayExpression") {
|
|
121
200
|
const types = p.value.elements.filter((el) => el?.type === "Identifier").map((el) => el.name);
|
|
@@ -124,7 +203,8 @@ function extractProps(obj, source) {
|
|
|
124
203
|
type: types.join(" | "),
|
|
125
204
|
required: false,
|
|
126
205
|
default: void 0,
|
|
127
|
-
description
|
|
206
|
+
description: jsdoc.description,
|
|
207
|
+
...tagFields
|
|
128
208
|
});
|
|
129
209
|
} else if (p.value.type === "ObjectExpression") {
|
|
130
210
|
let type = "unknown";
|
|
@@ -144,7 +224,8 @@ function extractProps(obj, source) {
|
|
|
144
224
|
type,
|
|
145
225
|
required,
|
|
146
226
|
default: defaultVal,
|
|
147
|
-
description
|
|
227
|
+
description: jsdoc.description,
|
|
228
|
+
...tagFields
|
|
148
229
|
});
|
|
149
230
|
}
|
|
150
231
|
}
|
|
@@ -171,14 +252,188 @@ function parseEmitJSDoc(comments) {
|
|
|
171
252
|
}
|
|
172
253
|
return map;
|
|
173
254
|
}
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
255
|
+
function extractTypeEmits(typeLiteral) {
|
|
256
|
+
const emits = [];
|
|
257
|
+
for (const member of typeLiteral.members) if (member.type === "TSPropertySignature") {
|
|
258
|
+
const name = member.key.type === "Identifier" ? member.key.name : member.key.type === "StringLiteral" ? member.key.value : "";
|
|
259
|
+
if (!name) continue;
|
|
260
|
+
const jsdoc = parseJSDocTags(member.leadingComments ?? []);
|
|
261
|
+
let payload;
|
|
262
|
+
const typeAnnotation = member.typeAnnotation?.typeAnnotation;
|
|
263
|
+
if (typeAnnotation?.type === "TSTupleType") payload = typeAnnotation.elementTypes.map((el) => {
|
|
264
|
+
if (el.type === "TSNamedTupleMember") return `${el.label?.name ?? "arg"}: ${resolveTypeString(el.elementType)}`;
|
|
265
|
+
return resolveTypeString(el);
|
|
266
|
+
}).join(", ");
|
|
267
|
+
emits.push({
|
|
268
|
+
name,
|
|
269
|
+
description: jsdoc.description,
|
|
270
|
+
...payload && { payload }
|
|
271
|
+
});
|
|
272
|
+
} else if (member.type === "TSCallSignatureDeclaration") {
|
|
273
|
+
const params = member.parameters ?? [];
|
|
274
|
+
if (params.length === 0) continue;
|
|
275
|
+
const firstParam = params[0];
|
|
276
|
+
if (firstParam?.typeAnnotation?.typeAnnotation?.type !== "TSLiteralType" || firstParam.typeAnnotation.typeAnnotation.literal.type !== "StringLiteral") continue;
|
|
277
|
+
const name = firstParam.typeAnnotation.typeAnnotation.literal.value;
|
|
278
|
+
const jsdoc = parseJSDocTags(member.leadingComments ?? []);
|
|
279
|
+
const payloadParams = params.slice(1);
|
|
280
|
+
let payload;
|
|
281
|
+
if (payloadParams.length > 0) payload = payloadParams.map((p) => {
|
|
282
|
+
return `${p.type === "Identifier" ? p.name : "arg"}: ${p.typeAnnotation?.typeAnnotation ? resolveTypeString(p.typeAnnotation.typeAnnotation) : "unknown"}`;
|
|
283
|
+
}).join(", ");
|
|
284
|
+
emits.push({
|
|
285
|
+
name,
|
|
286
|
+
description: jsdoc.description,
|
|
287
|
+
...payload && { payload }
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return emits;
|
|
291
|
+
}
|
|
292
|
+
function extractTypeSlots(typeLiteral) {
|
|
293
|
+
const slots = [];
|
|
294
|
+
for (const member of typeLiteral.members) {
|
|
295
|
+
const name = member.key?.type === "Identifier" ? member.key.name : member.key?.type === "StringLiteral" ? member.key.value : "";
|
|
296
|
+
if (!name) continue;
|
|
297
|
+
const jsdoc = parseJSDocTags(member.leadingComments ?? []);
|
|
298
|
+
const bindings = [];
|
|
299
|
+
if (member.type === "TSMethodSignature" && member.parameters?.length > 0) {
|
|
300
|
+
const propsType = member.parameters[0]?.typeAnnotation?.typeAnnotation;
|
|
301
|
+
if (propsType?.type === "TSTypeLiteral") {
|
|
302
|
+
for (const prop of propsType.members) if (prop.type === "TSPropertySignature") {
|
|
303
|
+
const propName = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : "";
|
|
304
|
+
const propType = prop.typeAnnotation?.typeAnnotation ? resolveTypeString(prop.typeAnnotation.typeAnnotation) : "unknown";
|
|
305
|
+
if (propName) bindings.push(`${propName}: ${propType}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
slots.push({
|
|
310
|
+
name,
|
|
311
|
+
description: jsdoc.description,
|
|
312
|
+
bindings
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return slots;
|
|
316
|
+
}
|
|
317
|
+
function extractExposes(obj, _source) {
|
|
318
|
+
const exposes = [];
|
|
319
|
+
for (const prop of obj.properties) {
|
|
320
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "ObjectMethod") continue;
|
|
321
|
+
const key = prop.key;
|
|
322
|
+
const name = key.type === "Identifier" ? key.name : key.type === "StringLiteral" ? key.value : "";
|
|
323
|
+
if (!name) continue;
|
|
324
|
+
const jsdoc = parseJSDocTags(prop.leadingComments ?? []);
|
|
325
|
+
exposes.push({
|
|
326
|
+
name,
|
|
327
|
+
type: "unknown",
|
|
328
|
+
description: jsdoc.description
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return exposes;
|
|
332
|
+
}
|
|
333
|
+
function extractComposables(ast) {
|
|
334
|
+
const seen = /* @__PURE__ */ new Set();
|
|
335
|
+
const composables = [];
|
|
336
|
+
for (const stmt of ast) {
|
|
337
|
+
const callNames = extractComposableCallNames(stmt);
|
|
338
|
+
for (const name of callNames) if (!seen.has(name)) {
|
|
339
|
+
seen.add(name);
|
|
340
|
+
composables.push({ name });
|
|
341
|
+
}
|
|
180
342
|
}
|
|
181
|
-
return
|
|
343
|
+
return composables;
|
|
344
|
+
}
|
|
345
|
+
function extractComposableCallNames(stmt) {
|
|
346
|
+
const names = [];
|
|
347
|
+
if (stmt.type === "ExpressionStatement" && stmt.expression.type === "CallExpression" && stmt.expression.callee.type === "Identifier" && /^use[A-Z]/.test(stmt.expression.callee.name)) names.push(stmt.expression.callee.name);
|
|
348
|
+
if (stmt.type === "VariableDeclaration") {
|
|
349
|
+
for (const decl of stmt.declarations) if (decl.init?.type === "CallExpression" && decl.init.callee.type === "Identifier" && /^use[A-Z]/.test(decl.init.callee.name)) names.push(decl.init.callee.name);
|
|
350
|
+
}
|
|
351
|
+
return names;
|
|
352
|
+
}
|
|
353
|
+
function extractTemplateSlots(templateAst) {
|
|
354
|
+
const slots = [];
|
|
355
|
+
walkTemplate(templateAst.children ?? [], slots);
|
|
356
|
+
return slots;
|
|
357
|
+
}
|
|
358
|
+
function walkTemplate(children, slots) {
|
|
359
|
+
for (const node of children) {
|
|
360
|
+
if (node.type === 1 && node.tag === "slot") {
|
|
361
|
+
let name = "default";
|
|
362
|
+
const bindings = [];
|
|
363
|
+
for (const prop of node.props ?? []) {
|
|
364
|
+
if (prop.type === 6 && prop.name === "name" && prop.value?.content) name = prop.value.content;
|
|
365
|
+
if (prop.type === 7 && prop.name === "bind" && prop.arg?.content) bindings.push(prop.arg.content);
|
|
366
|
+
}
|
|
367
|
+
slots.push({
|
|
368
|
+
name,
|
|
369
|
+
description: "",
|
|
370
|
+
bindings
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (node.children) walkTemplate(node.children, slots);
|
|
374
|
+
if (node.branches) {
|
|
375
|
+
for (const branch of node.branches) if (branch.children) walkTemplate(branch.children, slots);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function mergeSlots(typedSlots, templateSlots) {
|
|
380
|
+
const merged = [...typedSlots];
|
|
381
|
+
const typedNames = new Set(typedSlots.map((s) => s.name));
|
|
382
|
+
for (const ts of templateSlots) if (!typedNames.has(ts.name)) merged.push(ts);
|
|
383
|
+
return merged;
|
|
384
|
+
}
|
|
385
|
+
function extractOptionsAPI(ast, source) {
|
|
386
|
+
let props = [];
|
|
387
|
+
let emits = [];
|
|
388
|
+
for (const stmt of ast) {
|
|
389
|
+
if (stmt.type !== "ExportDefaultDeclaration") continue;
|
|
390
|
+
const decl = stmt.declaration;
|
|
391
|
+
if (decl.type !== "ObjectExpression") continue;
|
|
392
|
+
for (const prop of decl.properties) {
|
|
393
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
394
|
+
const key = prop.key;
|
|
395
|
+
const name = key.type === "Identifier" ? key.name : "";
|
|
396
|
+
if (name === "props") {
|
|
397
|
+
if (prop.value.type === "ObjectExpression") props = extractProps(prop.value, source);
|
|
398
|
+
else if (prop.value.type === "ArrayExpression") props = extractArrayProps(prop.value);
|
|
399
|
+
} else if (name === "emits") {
|
|
400
|
+
if (prop.value.type === "ArrayExpression") {
|
|
401
|
+
const comments = stmt.leadingComments ?? [];
|
|
402
|
+
emits = extractEmits(prop.value, comments);
|
|
403
|
+
} else if (prop.value.type === "ObjectExpression") emits = extractObjectEmits(prop.value);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
props,
|
|
410
|
+
emits
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function extractArrayProps(arr) {
|
|
414
|
+
const props = [];
|
|
415
|
+
for (const el of arr.elements) if (el?.type === "StringLiteral") props.push({
|
|
416
|
+
name: el.value,
|
|
417
|
+
type: "unknown",
|
|
418
|
+
required: false,
|
|
419
|
+
default: void 0,
|
|
420
|
+
description: ""
|
|
421
|
+
});
|
|
422
|
+
return props;
|
|
423
|
+
}
|
|
424
|
+
function extractObjectEmits(obj) {
|
|
425
|
+
const emits = [];
|
|
426
|
+
for (const prop of obj.properties) {
|
|
427
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
428
|
+
const name = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : "";
|
|
429
|
+
if (!name) continue;
|
|
430
|
+
const jsdoc = parseJSDocTags(prop.leadingComments ?? []);
|
|
431
|
+
emits.push({
|
|
432
|
+
name,
|
|
433
|
+
description: jsdoc.description
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return emits;
|
|
182
437
|
}
|
|
183
438
|
function stringifyDefault(node, source) {
|
|
184
439
|
switch (node.type) {
|
|
@@ -197,22 +452,46 @@ function stringifyDefault(node, source) {
|
|
|
197
452
|
//#region src/markdown.ts
|
|
198
453
|
function generateMarkdown(doc) {
|
|
199
454
|
const sections = [`# ${doc.name}`];
|
|
200
|
-
if (doc.
|
|
201
|
-
|
|
455
|
+
if (doc.description) sections.push("", doc.description);
|
|
456
|
+
const hasProps = doc.props.length > 0;
|
|
457
|
+
const hasEmits = doc.emits.length > 0;
|
|
458
|
+
const hasSlots = (doc.slots?.length ?? 0) > 0;
|
|
459
|
+
const hasExposes = (doc.exposes?.length ?? 0) > 0;
|
|
460
|
+
const hasComposables = (doc.composables?.length ?? 0) > 0;
|
|
461
|
+
if (!hasProps && !hasEmits && !hasSlots && !hasExposes && !hasComposables) {
|
|
462
|
+
sections.push("", "No documentable API found.");
|
|
202
463
|
return sections.join("\n") + "\n";
|
|
203
464
|
}
|
|
204
|
-
if (
|
|
465
|
+
if (hasProps) {
|
|
205
466
|
sections.push("", "## Props", "");
|
|
206
467
|
sections.push("| Name | Type | Required | Default | Description |");
|
|
207
468
|
sections.push("| --- | --- | --- | --- | --- |");
|
|
469
|
+
const examples = [];
|
|
208
470
|
for (const p of doc.props) {
|
|
209
471
|
const def = p.default !== void 0 ? `\`${p.default}\`` : "-";
|
|
210
|
-
|
|
472
|
+
let desc = p.description || "-";
|
|
473
|
+
if (p.deprecated) desc += typeof p.deprecated === "string" && p.deprecated ? ` **Deprecated**: ${p.deprecated}` : " **Deprecated**";
|
|
474
|
+
if (p.since) desc += ` *(since ${p.since})*`;
|
|
475
|
+
if (p.see) desc += ` See: ${p.see}`;
|
|
211
476
|
const req = p.required ? "Yes" : "No";
|
|
212
477
|
sections.push(`| ${p.name} | ${p.type} | ${req} | ${def} | ${desc} |`);
|
|
478
|
+
if (p.example) examples.push({
|
|
479
|
+
name: p.name,
|
|
480
|
+
example: p.example
|
|
481
|
+
});
|
|
213
482
|
}
|
|
483
|
+
for (const { name, example } of examples) sections.push("", `**\`${name}\` example:**`, "", "```", example, "```");
|
|
214
484
|
}
|
|
215
|
-
if (doc.emits.
|
|
485
|
+
if (hasEmits) if (doc.emits.some((e) => e.payload)) {
|
|
486
|
+
sections.push("", "## Emits", "");
|
|
487
|
+
sections.push("| Name | Payload | Description |");
|
|
488
|
+
sections.push("| --- | --- | --- |");
|
|
489
|
+
for (const e of doc.emits) {
|
|
490
|
+
const desc = e.description || "-";
|
|
491
|
+
const payload = e.payload || "-";
|
|
492
|
+
sections.push(`| ${e.name} | ${payload} | ${desc} |`);
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
216
495
|
sections.push("", "## Emits", "");
|
|
217
496
|
sections.push("| Name | Description |");
|
|
218
497
|
sections.push("| --- | --- |");
|
|
@@ -221,6 +500,29 @@ function generateMarkdown(doc) {
|
|
|
221
500
|
sections.push(`| ${e.name} | ${desc} |`);
|
|
222
501
|
}
|
|
223
502
|
}
|
|
503
|
+
if (hasSlots) {
|
|
504
|
+
sections.push("", "## Slots", "");
|
|
505
|
+
sections.push("| Name | Bindings | Description |");
|
|
506
|
+
sections.push("| --- | --- | --- |");
|
|
507
|
+
for (const s of doc.slots) {
|
|
508
|
+
const desc = s.description || "-";
|
|
509
|
+
const bindings = s.bindings.length > 0 ? s.bindings.join(", ") : "-";
|
|
510
|
+
sections.push(`| ${s.name} | ${bindings} | ${desc} |`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (hasExposes) {
|
|
514
|
+
sections.push("", "## Exposed", "");
|
|
515
|
+
sections.push("| Name | Type | Description |");
|
|
516
|
+
sections.push("| --- | --- | --- |");
|
|
517
|
+
for (const e of doc.exposes) {
|
|
518
|
+
const desc = e.description || "-";
|
|
519
|
+
sections.push(`| ${e.name} | ${e.type} | ${desc} |`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (hasComposables) {
|
|
523
|
+
sections.push("", "## Composables Used", "");
|
|
524
|
+
for (const c of doc.composables) sections.push(`- \`${c.name}\``);
|
|
525
|
+
}
|
|
224
526
|
return sections.join("\n") + "\n";
|
|
225
527
|
}
|
|
226
528
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compmark-vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Auto-generate Markdown documentation from Vue 3 SFCs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "noopurphalak/compmark-vue",
|
|
7
7
|
"bin": {
|
|
8
|
-
"compmark
|
|
8
|
+
"compmark": "./dist/cli.mjs"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist"
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"prepack": "pnpm build",
|
|
25
25
|
"release": "pnpm test && pnpm build && changelogen --release && npm publish && git push --follow-tags",
|
|
26
26
|
"test": "pnpm lint && pnpm typecheck && vitest run --coverage",
|
|
27
|
-
"typecheck": "tsgo --noEmit --skipLibCheck"
|
|
27
|
+
"typecheck": "tsgo --noEmit --skipLibCheck",
|
|
28
|
+
"prepare": "husky"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"@vue/compiler-sfc": "^3.5.0"
|
|
@@ -36,11 +37,19 @@
|
|
|
36
37
|
"@vitest/coverage-v8": "latest",
|
|
37
38
|
"automd": "latest",
|
|
38
39
|
"changelogen": "latest",
|
|
40
|
+
"husky": "^9.1.7",
|
|
41
|
+
"lint-staged": "^16.3.3",
|
|
39
42
|
"obuild": "latest",
|
|
40
43
|
"oxfmt": "latest",
|
|
41
44
|
"oxlint": "latest",
|
|
42
45
|
"typescript": "latest",
|
|
43
46
|
"vitest": "latest"
|
|
44
47
|
},
|
|
48
|
+
"lint-staged": {
|
|
49
|
+
"*.{ts,vue}": [
|
|
50
|
+
"oxlint --fix",
|
|
51
|
+
"oxfmt"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
45
54
|
"packageManager": "pnpm@10.29.3"
|
|
46
55
|
}
|