compmark-vue 0.2.6 → 0.3.1
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 +184 -7
- package/dist/cli.mjs +359 -34
- package/dist/index.d.mts +30 -1
- package/dist/index.mjs +166 -7
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -9,17 +9,105 @@
|
|
|
9
9
|
|
|
10
10
|
Auto-generate Markdown documentation from Vue 3 SFCs. Zero configuration required.
|
|
11
11
|
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
# npm
|
|
16
|
+
npm install -D compmark-vue
|
|
17
|
+
|
|
18
|
+
# pnpm
|
|
19
|
+
pnpm add -D compmark-vue
|
|
20
|
+
|
|
21
|
+
# yarn
|
|
22
|
+
yarn add -D compmark-vue
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> Requires Node.js >= 20
|
|
26
|
+
|
|
12
27
|
## Quick Start
|
|
13
28
|
|
|
29
|
+
Document a single component:
|
|
30
|
+
|
|
14
31
|
```sh
|
|
15
32
|
npx compmark-vue ./src/components/Button.vue
|
|
16
33
|
```
|
|
17
34
|
|
|
18
|
-
|
|
35
|
+
Document an entire directory:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npx compmark-vue ./src/components --out ./docs/api
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Add to your `package.json`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"scripts": {
|
|
46
|
+
"docs": "compmark ./src/components --out ./docs/api"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
compmark <files/dirs/globs> [options]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| Option | Description | Default |
|
|
58
|
+
| --------------------- | ------------------------------- | ------- |
|
|
59
|
+
| `--out <dir>` | Output directory | `.` |
|
|
60
|
+
| `--format <md\|json>` | Output format | `md` |
|
|
61
|
+
| `--join` | Combine into a single file | |
|
|
62
|
+
| `--ignore <patterns>` | Comma-separated ignore patterns | |
|
|
63
|
+
| `--watch` | Watch for changes and rebuild | |
|
|
64
|
+
| `--silent` | Suppress non-error output | |
|
|
65
|
+
|
|
66
|
+
### Examples
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
# Single file
|
|
70
|
+
compmark Button.vue
|
|
71
|
+
|
|
72
|
+
# Directory (recursive)
|
|
73
|
+
compmark src/components --out docs/api
|
|
74
|
+
|
|
75
|
+
# Glob pattern
|
|
76
|
+
compmark "src/**/components/*.vue" --out docs
|
|
77
|
+
|
|
78
|
+
# Monorepo
|
|
79
|
+
compmark "packages/*/src/components" --out docs/api
|
|
80
|
+
|
|
81
|
+
# Combined markdown with table of contents
|
|
82
|
+
compmark src/components --out docs --join
|
|
83
|
+
|
|
84
|
+
# JSON output
|
|
85
|
+
compmark src/components --out docs/api --format json
|
|
86
|
+
|
|
87
|
+
# JSON combined into single file
|
|
88
|
+
compmark src/components --format json --join --out docs
|
|
89
|
+
|
|
90
|
+
# Ignore patterns
|
|
91
|
+
compmark src/components --ignore "internal,*.test"
|
|
92
|
+
|
|
93
|
+
# Watch mode
|
|
94
|
+
compmark src/components --out docs --watch
|
|
95
|
+
|
|
96
|
+
# Multiple inputs
|
|
97
|
+
compmark src/components src/layouts --out docs
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The summary line shows what happened:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
✓ 24 components documented, 2 skipped, 0 errors
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Exit code is `1` when errors occur (except in watch mode).
|
|
19
107
|
|
|
20
108
|
## Features
|
|
21
109
|
|
|
22
|
-
- [Props](#props) — runtime and TypeScript generic syntax
|
|
110
|
+
- [Props](#props) — runtime and TypeScript generic syntax, including imported types
|
|
23
111
|
- [Emits](#emits) — array, TypeScript property, and call signature syntax
|
|
24
112
|
- [Slots](#slots) — `defineSlots` with typed bindings, template `<slot>` fallback
|
|
25
113
|
- [Expose](#expose) — `defineExpose` with JSDoc descriptions
|
|
@@ -27,6 +115,7 @@ This parses the component and creates `Button.md` in your current directory.
|
|
|
27
115
|
- [JSDoc tags](#jsdoc-tags) — `@deprecated`, `@since`, `@example`, `@see`, `@default`
|
|
28
116
|
- [`@internal`](#internal-components) — exclude components from output
|
|
29
117
|
- [Options API](#options-api) — `export default { props, emits }` support
|
|
118
|
+
- [Output formats](#output-formats) — Markdown (individual or joined), JSON
|
|
30
119
|
- Empty sections are skipped cleanly — no placeholder noise
|
|
31
120
|
|
|
32
121
|
## Examples
|
|
@@ -83,6 +172,29 @@ defineProps({
|
|
|
83
172
|
</script>
|
|
84
173
|
```
|
|
85
174
|
|
|
175
|
+
#### Imported types
|
|
176
|
+
|
|
177
|
+
`defineProps<ImportedType>()` with exported interfaces or type aliases is supported:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// types.ts
|
|
181
|
+
export interface ButtonProps {
|
|
182
|
+
/** The label text */
|
|
183
|
+
label: string;
|
|
184
|
+
disabled?: boolean;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```vue
|
|
189
|
+
<script setup lang="ts">
|
|
190
|
+
import type { ButtonProps } from "./types";
|
|
191
|
+
|
|
192
|
+
defineProps<ButtonProps>();
|
|
193
|
+
</script>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Interface `extends` is resolved (up to 5 levels deep). `withDefaults` works with imported types too.
|
|
197
|
+
|
|
86
198
|
### Emits
|
|
87
199
|
|
|
88
200
|
TypeScript generic syntax with payloads:
|
|
@@ -186,15 +298,18 @@ Output:
|
|
|
186
298
|
|
|
187
299
|
### Composables
|
|
188
300
|
|
|
189
|
-
Any `useX()` calls in `<script setup>` are automatically detected:
|
|
301
|
+
Any `useX()` calls in `<script setup>` are automatically detected. Variable bindings (simple assignment, object/array destructuring, rest elements) are extracted:
|
|
190
302
|
|
|
191
303
|
```vue
|
|
192
304
|
<script setup lang="ts">
|
|
193
305
|
import { useRouter } from "vue-router";
|
|
194
306
|
import { useMouse } from "@vueuse/core";
|
|
307
|
+
import { useAuth } from "./composables/useAuth";
|
|
195
308
|
|
|
196
309
|
const router = useRouter();
|
|
197
310
|
const { x, y } = useMouse();
|
|
311
|
+
const { user, login, logout } = useAuth();
|
|
312
|
+
useHead({ title: "My App" });
|
|
198
313
|
</script>
|
|
199
314
|
```
|
|
200
315
|
|
|
@@ -203,10 +318,31 @@ Output:
|
|
|
203
318
|
```md
|
|
204
319
|
## Composables Used
|
|
205
320
|
|
|
206
|
-
|
|
207
|
-
|
|
321
|
+
### `useRouter`
|
|
322
|
+
|
|
323
|
+
**Returns:** `router`
|
|
324
|
+
|
|
325
|
+
### `useMouse`
|
|
326
|
+
|
|
327
|
+
**Returns:** `x`, `y`
|
|
328
|
+
|
|
329
|
+
### `useAuth`
|
|
330
|
+
|
|
331
|
+
_Source: `./composables/useAuth`_
|
|
332
|
+
|
|
333
|
+
| Variable | Type |
|
|
334
|
+
| -------- | ------------------------------------------- |
|
|
335
|
+
| user | Ref<User> |
|
|
336
|
+
| login | (credentials: Credentials) => Promise<void> |
|
|
337
|
+
| logout | () => void |
|
|
338
|
+
|
|
339
|
+
### `useHead`
|
|
340
|
+
|
|
341
|
+
Called for side effects.
|
|
208
342
|
```
|
|
209
343
|
|
|
344
|
+
For local imports (`./` or `@/` paths), types are automatically resolved from the composable source file — `ref()`, `computed()`, `reactive()`, function signatures, and literals are all inferred. Source attribution is shown for local imports only.
|
|
345
|
+
|
|
210
346
|
### JSDoc Tags
|
|
211
347
|
|
|
212
348
|
Props support `@deprecated`, `@since`, `@example`, and `@see`:
|
|
@@ -259,7 +395,8 @@ defineProps<{
|
|
|
259
395
|
|
|
260
396
|
```sh
|
|
261
397
|
$ compmark InternalHelper.vue
|
|
262
|
-
Skipped InternalHelper.vue (marked @internal)
|
|
398
|
+
Skipped InternalHelper.vue (marked @internal)
|
|
399
|
+
✓ 0 components documented, 1 skipped, 0 errors
|
|
263
400
|
```
|
|
264
401
|
|
|
265
402
|
### Options API
|
|
@@ -303,6 +440,36 @@ Output:
|
|
|
303
440
|
| update | - |
|
|
304
441
|
```
|
|
305
442
|
|
|
443
|
+
### Output Formats
|
|
444
|
+
|
|
445
|
+
**Individual markdown** (default) — one `.md` file per component:
|
|
446
|
+
|
|
447
|
+
```sh
|
|
448
|
+
compmark src/components --out docs
|
|
449
|
+
# Creates: docs/Button.md, docs/Dialog.md, ...
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Joined markdown** — single file with table of contents:
|
|
453
|
+
|
|
454
|
+
```sh
|
|
455
|
+
compmark src/components --out docs --join
|
|
456
|
+
# Creates: docs/components.md
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
The joined file includes a generated timestamp, table of contents with anchor links, and all components with headings bumped one level.
|
|
460
|
+
|
|
461
|
+
**JSON** — machine-readable output:
|
|
462
|
+
|
|
463
|
+
```sh
|
|
464
|
+
# Individual JSON files
|
|
465
|
+
compmark src/components --out docs --format json
|
|
466
|
+
# Creates: docs/Button.json, docs/Dialog.json, ...
|
|
467
|
+
|
|
468
|
+
# Combined JSON
|
|
469
|
+
compmark src/components --format json --join --out docs
|
|
470
|
+
# Creates: docs/components.json with { generated, components: [...] }
|
|
471
|
+
```
|
|
472
|
+
|
|
306
473
|
## Programmatic API
|
|
307
474
|
|
|
308
475
|
```sh
|
|
@@ -325,6 +492,16 @@ const doc = parseSFC(source, "Button.vue");
|
|
|
325
492
|
const md = generateMarkdown(doc);
|
|
326
493
|
```
|
|
327
494
|
|
|
495
|
+
Multi-file processing:
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
import { discoverFiles, processFiles } from "compmark-vue";
|
|
499
|
+
|
|
500
|
+
const files = await discoverFiles(["src/components"], ["dist"]);
|
|
501
|
+
const summary = processFiles(files, { silent: false });
|
|
502
|
+
// summary.files, summary.documented, summary.skipped, summary.errors
|
|
503
|
+
```
|
|
504
|
+
|
|
328
505
|
## Development
|
|
329
506
|
|
|
330
507
|
<details>
|
|
@@ -332,7 +509,7 @@ const md = generateMarkdown(doc);
|
|
|
332
509
|
<summary>local development</summary>
|
|
333
510
|
|
|
334
511
|
- Clone this repository
|
|
335
|
-
- Install latest LTS version of [Node.js](https://nodejs.org/en/)
|
|
512
|
+
- Install latest LTS version of [Node.js](https://nodejs.org/en/) (>= 20)
|
|
336
513
|
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
|
|
337
514
|
- Install dependencies using `pnpm install`
|
|
338
515
|
- Run interactive tests using `pnpm dev`
|
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, statSync, watch, writeFileSync } from "node:fs";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { glob } from "tinyglobby";
|
|
4
6
|
import { babelParse, compileScript, parse } from "@vue/compiler-sfc";
|
|
7
|
+
//#region src/discovery.ts
|
|
8
|
+
async function discoverFiles(inputs, ignore) {
|
|
9
|
+
const baseIgnore = ["**/node_modules/**"];
|
|
10
|
+
const userIgnore = (ignore ?? []).map(normalizeIgnorePattern);
|
|
11
|
+
const allIgnore = [...baseIgnore, ...userIgnore];
|
|
12
|
+
const found = /* @__PURE__ */ new Set();
|
|
13
|
+
for (const input of inputs) {
|
|
14
|
+
const resolved = resolve(input);
|
|
15
|
+
if (input.endsWith(".vue") && existsSync(resolved)) found.add(resolved);
|
|
16
|
+
else if (isDirectory$1(resolved)) {
|
|
17
|
+
const files = await glob("**/*.vue", {
|
|
18
|
+
cwd: resolved,
|
|
19
|
+
absolute: true,
|
|
20
|
+
ignore: allIgnore
|
|
21
|
+
});
|
|
22
|
+
for (const f of files) found.add(f);
|
|
23
|
+
} else {
|
|
24
|
+
const files = await glob(input, {
|
|
25
|
+
absolute: true,
|
|
26
|
+
ignore: allIgnore
|
|
27
|
+
});
|
|
28
|
+
for (const f of files) found.add(f);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [...found].sort();
|
|
32
|
+
}
|
|
33
|
+
function isDirectory$1(path) {
|
|
34
|
+
try {
|
|
35
|
+
return statSync(path).isDirectory();
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function normalizeIgnorePattern(pattern) {
|
|
41
|
+
if (pattern.includes("*") || pattern.includes("/")) return pattern;
|
|
42
|
+
return `**/${pattern}/**`;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
5
45
|
//#region src/resolver.ts
|
|
6
46
|
function resolveImportPath(importSource, sfcDir) {
|
|
7
47
|
try {
|
|
@@ -265,6 +305,56 @@ function resolveTypeAnnotation(node) {
|
|
|
265
305
|
}
|
|
266
306
|
}
|
|
267
307
|
//#endregion
|
|
308
|
+
//#region src/type-resolver.ts
|
|
309
|
+
function resolveImportedPropsType(typeName, importMap, sfcDir) {
|
|
310
|
+
const source = importMap.get(typeName);
|
|
311
|
+
if (!source) return null;
|
|
312
|
+
const resolvedPath = resolveImportPath(source, sfcDir);
|
|
313
|
+
if (!resolvedPath) return null;
|
|
314
|
+
try {
|
|
315
|
+
return findExportedType(babelParse(readFileSync(resolvedPath, "utf-8"), {
|
|
316
|
+
plugins: ["typescript"],
|
|
317
|
+
sourceType: "module"
|
|
318
|
+
}).program.body, typeName, 0);
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function findExportedType(stmts, typeName, depth) {
|
|
324
|
+
if (depth > 5) return null;
|
|
325
|
+
for (const stmt of stmts) {
|
|
326
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "TSInterfaceDeclaration" && stmt.declaration.id.name === typeName) return resolveInterfaceMembers(stmt.declaration, stmts, depth);
|
|
327
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "TSTypeAliasDeclaration" && stmt.declaration.id.name === typeName && stmt.declaration.typeAnnotation.type === "TSTypeLiteral") return { members: [...stmt.declaration.typeAnnotation.members] };
|
|
328
|
+
}
|
|
329
|
+
if (hasNamedExport(stmts, typeName)) return findTypeInFile(stmts, typeName, depth);
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
function findTypeInFile(stmts, typeName, depth) {
|
|
333
|
+
if (depth > 5) return null;
|
|
334
|
+
for (const stmt of stmts) {
|
|
335
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "TSInterfaceDeclaration" && stmt.declaration.id.name === typeName) return resolveInterfaceMembers(stmt.declaration, stmts, depth);
|
|
336
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "TSTypeAliasDeclaration" && stmt.declaration.id.name === typeName && stmt.declaration.typeAnnotation.type === "TSTypeLiteral") return { members: [...stmt.declaration.typeAnnotation.members] };
|
|
337
|
+
if (stmt.type === "TSInterfaceDeclaration" && stmt.id.name === typeName) return resolveInterfaceMembers(stmt, stmts, depth);
|
|
338
|
+
if (stmt.type === "TSTypeAliasDeclaration" && stmt.id.name === typeName && stmt.typeAnnotation.type === "TSTypeLiteral") return { members: [...stmt.typeAnnotation.members] };
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
function resolveInterfaceMembers(decl, stmts, depth) {
|
|
343
|
+
const members = [];
|
|
344
|
+
if (decl.extends) for (const ext of decl.extends) {
|
|
345
|
+
const parentName = ext.expression?.type === "Identifier" ? ext.expression.name : null;
|
|
346
|
+
if (parentName) {
|
|
347
|
+
const parent = findTypeInFile(stmts, parentName, depth + 1);
|
|
348
|
+
if (parent) members.push(...parent.members);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
members.push(...decl.body.body);
|
|
352
|
+
return { members };
|
|
353
|
+
}
|
|
354
|
+
function hasNamedExport(stmts, name) {
|
|
355
|
+
return stmts.some((s) => s.type === "ExportNamedDeclaration" && !s.declaration && s.specifiers.some((spec) => spec.type === "ExportSpecifier" && (spec.local.type === "Identifier" && spec.local.name === name || spec.exported.type === "Identifier" && spec.exported.name === name)));
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
268
358
|
//#region src/parser.ts
|
|
269
359
|
function parseJSDocTags(comments) {
|
|
270
360
|
const result = { description: "" };
|
|
@@ -291,29 +381,55 @@ function parseSFC(source, filename, sfcDir) {
|
|
|
291
381
|
props: [],
|
|
292
382
|
emits: []
|
|
293
383
|
};
|
|
294
|
-
const
|
|
384
|
+
const fullPath = sfcDir ? `${sfcDir}/${filename}` : filename;
|
|
385
|
+
const { descriptor } = parse(source, { filename: fullPath });
|
|
386
|
+
doc.scriptSetup = !!descriptor.scriptSetup;
|
|
295
387
|
if (descriptor.template?.ast) {
|
|
296
388
|
const templateSlots = extractTemplateSlots(descriptor.template.ast);
|
|
297
389
|
if (templateSlots.length > 0) doc.slots = templateSlots;
|
|
298
390
|
}
|
|
299
391
|
if (!descriptor.scriptSetup && !descriptor.script) return doc;
|
|
300
|
-
|
|
392
|
+
let compiled;
|
|
393
|
+
try {
|
|
394
|
+
compiled = compileScript(descriptor, {
|
|
395
|
+
id: fullPath,
|
|
396
|
+
fs: {
|
|
397
|
+
fileExists: (file) => existsSync(file),
|
|
398
|
+
readFile: (file) => {
|
|
399
|
+
try {
|
|
400
|
+
return readFileSync(file, "utf-8");
|
|
401
|
+
} catch {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
} catch {
|
|
408
|
+
return doc;
|
|
409
|
+
}
|
|
301
410
|
const componentJSDoc = extractComponentJSDoc(compiled.scriptSetupAst ?? compiled.scriptAst ?? []);
|
|
302
411
|
doc.description = componentJSDoc.description;
|
|
303
412
|
doc.internal = componentJSDoc.internal;
|
|
304
413
|
const setupAst = compiled.scriptSetupAst;
|
|
305
414
|
if (setupAst) {
|
|
306
415
|
const scriptSource = descriptor.scriptSetup?.content ?? compiled.content;
|
|
416
|
+
const importMap = buildImportMap(setupAst);
|
|
307
417
|
for (const stmt of setupAst) {
|
|
308
418
|
const calls = extractDefineCalls(stmt);
|
|
309
419
|
for (const { callee, args, leadingComments, typeParams, defaultsArg } of calls) if (callee === "defineProps" && args[0]?.type === "ObjectExpression") doc.props = extractProps(args[0], scriptSource);
|
|
310
420
|
else if (callee === "defineProps" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.props = extractTypeProps(typeParams.params[0], defaultsArg, scriptSource);
|
|
311
|
-
else if (callee === "
|
|
421
|
+
else if (callee === "defineProps" && typeParams?.params[0]?.type === "TSTypeReference") {
|
|
422
|
+
const typeName = typeParams.params[0].typeName?.name;
|
|
423
|
+
if (typeName && sfcDir) {
|
|
424
|
+
const resolved = resolveImportedPropsType(typeName, importMap, sfcDir);
|
|
425
|
+
if (resolved) doc.props = extractTypeProps(resolved, defaultsArg, scriptSource);
|
|
426
|
+
}
|
|
427
|
+
} else if (callee === "defineEmits" && args[0]?.type === "ArrayExpression") doc.emits = extractEmits(args[0], leadingComments);
|
|
312
428
|
else if (callee === "defineEmits" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.emits = extractTypeEmits(typeParams.params[0]);
|
|
313
429
|
else if (callee === "defineSlots" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.slots = extractTypeSlots(typeParams.params[0]);
|
|
314
430
|
else if (callee === "defineExpose" && args[0]?.type === "ObjectExpression") doc.exposes = extractExposes(args[0], scriptSource);
|
|
315
431
|
}
|
|
316
|
-
doc.composables = extractComposables(setupAst,
|
|
432
|
+
doc.composables = extractComposables(setupAst, importMap, sfcDir);
|
|
317
433
|
}
|
|
318
434
|
const scriptAst = compiled.scriptAst;
|
|
319
435
|
if (scriptAst && doc.props.length === 0 && doc.emits.length === 0) {
|
|
@@ -785,6 +901,7 @@ function escHtml(value) {
|
|
|
785
901
|
function generateMarkdown(doc) {
|
|
786
902
|
const sections = [`# ${doc.name}`];
|
|
787
903
|
if (doc.description) sections.push("", doc.description);
|
|
904
|
+
if (doc.scriptSetup) sections.push("", "**Note:** Uses `<script setup>` syntax.");
|
|
788
905
|
const hasProps = doc.props.length > 0;
|
|
789
906
|
const hasEmits = doc.emits.length > 0;
|
|
790
907
|
const hasSlots = (doc.slots?.length ?? 0) > 0;
|
|
@@ -873,6 +990,12 @@ function generateMarkdown(doc) {
|
|
|
873
990
|
}
|
|
874
991
|
return sections.join("\n") + "\n";
|
|
875
992
|
}
|
|
993
|
+
function adjustHeadingLevel(md, increment) {
|
|
994
|
+
return md.replace(/^(#{1,6})\s/gm, (_, hashes) => {
|
|
995
|
+
const newLevel = Math.min(hashes.length + increment, 6);
|
|
996
|
+
return "#".repeat(newLevel) + " ";
|
|
997
|
+
});
|
|
998
|
+
}
|
|
876
999
|
//#endregion
|
|
877
1000
|
//#region src/index.ts
|
|
878
1001
|
function parseComponent(filePath) {
|
|
@@ -880,37 +1003,239 @@ function parseComponent(filePath) {
|
|
|
880
1003
|
return parseSFC(readFileSync(abs, "utf-8"), abs.split("/").pop() ?? "Unknown.vue", abs.substring(0, abs.lastIndexOf("/")));
|
|
881
1004
|
}
|
|
882
1005
|
//#endregion
|
|
883
|
-
//#region src/
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
if (
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1006
|
+
//#region src/runner.ts
|
|
1007
|
+
function processFiles(filePaths, options) {
|
|
1008
|
+
const summary = {
|
|
1009
|
+
documented: 0,
|
|
1010
|
+
skipped: 0,
|
|
1011
|
+
errors: 0,
|
|
1012
|
+
files: [],
|
|
1013
|
+
errorDetails: []
|
|
1014
|
+
};
|
|
1015
|
+
for (const filePath of filePaths) try {
|
|
1016
|
+
const doc = parseComponent(filePath);
|
|
1017
|
+
if (doc.internal) {
|
|
1018
|
+
summary.skipped++;
|
|
1019
|
+
if (!options.silent) {
|
|
1020
|
+
const name = filePath.split("/").pop() ?? filePath;
|
|
1021
|
+
console.log(` Skipped ${name} (marked @internal)`);
|
|
1022
|
+
}
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
summary.documented++;
|
|
1026
|
+
summary.files.push({
|
|
1027
|
+
path: filePath,
|
|
1028
|
+
doc
|
|
1029
|
+
});
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
summary.errors++;
|
|
1032
|
+
const name = filePath.split("/").pop() ?? filePath;
|
|
1033
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1034
|
+
summary.errorDetails.push({
|
|
1035
|
+
path: filePath,
|
|
1036
|
+
error: message
|
|
1037
|
+
});
|
|
1038
|
+
if (!options.silent) console.warn(` Warning: Could not parse ${name}: ${message}`);
|
|
1039
|
+
}
|
|
1040
|
+
return summary;
|
|
1041
|
+
}
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region src/output.ts
|
|
1044
|
+
function writeIndividualMarkdown(results, outDir, silent) {
|
|
1045
|
+
mkdirSync(outDir, { recursive: true });
|
|
1046
|
+
const usedNames = /* @__PURE__ */ new Map();
|
|
1047
|
+
for (const { doc } of results) {
|
|
1048
|
+
const baseName = doc.name;
|
|
1049
|
+
const count = usedNames.get(baseName) ?? 0;
|
|
1050
|
+
usedNames.set(baseName, count + 1);
|
|
1051
|
+
const fileName = count === 0 ? baseName : `${baseName}-${count + 1}`;
|
|
1052
|
+
const md = generateMarkdown(doc);
|
|
1053
|
+
writeFileSync(join(outDir, `${fileName}.md`), md, "utf-8");
|
|
1054
|
+
if (!silent) console.log(` Created ${fileName}.md`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function writeJoinedMarkdown(results, outDir, silent) {
|
|
1058
|
+
mkdirSync(outDir, { recursive: true });
|
|
1059
|
+
const sections = [];
|
|
1060
|
+
sections.push("# Component Documentation");
|
|
1061
|
+
sections.push("");
|
|
1062
|
+
sections.push(`*Generated: ${(/* @__PURE__ */ new Date()).toISOString()}*`);
|
|
1063
|
+
sections.push("");
|
|
1064
|
+
sections.push("## Table of Contents");
|
|
1065
|
+
sections.push("");
|
|
1066
|
+
for (const { doc } of results) {
|
|
1067
|
+
const anchor = doc.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1068
|
+
sections.push(`- [${doc.name}](#${anchor})`);
|
|
1069
|
+
}
|
|
1070
|
+
for (const { doc } of results) {
|
|
1071
|
+
const adjusted = adjustHeadingLevel(generateMarkdown(doc), 1);
|
|
1072
|
+
sections.push("");
|
|
1073
|
+
sections.push("---");
|
|
1074
|
+
sections.push("");
|
|
1075
|
+
sections.push(adjusted.trimEnd());
|
|
1076
|
+
}
|
|
1077
|
+
writeFileSync(join(outDir, "components.md"), sections.join("\n") + "\n", "utf-8");
|
|
1078
|
+
if (!silent) console.log(` Created components.md`);
|
|
1079
|
+
}
|
|
1080
|
+
function writeJSON(results, outDir, joined, silent) {
|
|
1081
|
+
mkdirSync(outDir, { recursive: true });
|
|
1082
|
+
if (joined) {
|
|
1083
|
+
const data = {
|
|
1084
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1085
|
+
components: results.map((r) => r.doc)
|
|
1086
|
+
};
|
|
1087
|
+
writeFileSync(join(outDir, "components.json"), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1088
|
+
if (!silent) console.log(` Created components.json`);
|
|
1089
|
+
} else {
|
|
1090
|
+
const usedNames = /* @__PURE__ */ new Map();
|
|
1091
|
+
for (const { doc } of results) {
|
|
1092
|
+
const baseName = doc.name;
|
|
1093
|
+
const count = usedNames.get(baseName) ?? 0;
|
|
1094
|
+
usedNames.set(baseName, count + 1);
|
|
1095
|
+
const fileName = count === 0 ? baseName : `${baseName}-${count + 1}`;
|
|
1096
|
+
writeFileSync(join(outDir, `${fileName}.json`), JSON.stringify(doc, null, 2) + "\n", "utf-8");
|
|
1097
|
+
if (!silent) console.log(` Created ${fileName}.json`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
//#endregion
|
|
1102
|
+
//#region src/watcher.ts
|
|
1103
|
+
function startWatcher(inputs, ignore, rebuild) {
|
|
1104
|
+
const roots = /* @__PURE__ */ new Set();
|
|
1105
|
+
for (const input of inputs) if (input.endsWith(".vue")) roots.add(dirname(resolve(input)));
|
|
1106
|
+
else roots.add(resolve(input));
|
|
1107
|
+
let timer = null;
|
|
1108
|
+
const debounce = () => {
|
|
1109
|
+
if (timer) clearTimeout(timer);
|
|
1110
|
+
timer = setTimeout(() => {
|
|
1111
|
+
console.log("[watch] Rebuilding...");
|
|
1112
|
+
try {
|
|
1113
|
+
rebuild();
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1116
|
+
console.error(`[watch] Error: ${msg}`);
|
|
1117
|
+
}
|
|
1118
|
+
console.log("[watch] Done.");
|
|
1119
|
+
}, 300);
|
|
1120
|
+
};
|
|
1121
|
+
const watchers = [];
|
|
1122
|
+
for (const root of roots) try {
|
|
1123
|
+
const watcher = watch(root, { recursive: true }, (_event, filename) => {
|
|
1124
|
+
if (!filename || !filename.endsWith(".vue")) return;
|
|
1125
|
+
if (ignore.some((pattern) => filename.includes(pattern))) return;
|
|
1126
|
+
debounce();
|
|
1127
|
+
});
|
|
1128
|
+
watchers.push(watcher);
|
|
1129
|
+
} catch {
|
|
1130
|
+
console.warn(`[watch] Could not watch: ${root}`);
|
|
1131
|
+
}
|
|
1132
|
+
console.log(`[watch] Watching ${roots.size} root(s) for changes...`);
|
|
1133
|
+
process.on("SIGINT", () => {
|
|
1134
|
+
for (const w of watchers) w.close();
|
|
903
1135
|
process.exit(0);
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
//#endregion
|
|
1139
|
+
//#region src/cli.ts
|
|
1140
|
+
const main = defineCommand({
|
|
1141
|
+
meta: {
|
|
1142
|
+
name: "compmark",
|
|
1143
|
+
version: "0.3.0",
|
|
1144
|
+
description: "Auto-generate Markdown documentation from Vue 3 SFCs"
|
|
1145
|
+
},
|
|
1146
|
+
args: {
|
|
1147
|
+
out: {
|
|
1148
|
+
type: "string",
|
|
1149
|
+
description: "Output directory",
|
|
1150
|
+
default: "."
|
|
1151
|
+
},
|
|
1152
|
+
ignore: {
|
|
1153
|
+
type: "string",
|
|
1154
|
+
description: "Comma-separated ignore patterns"
|
|
1155
|
+
},
|
|
1156
|
+
join: {
|
|
1157
|
+
type: "boolean",
|
|
1158
|
+
description: "Combine output into a single file"
|
|
1159
|
+
},
|
|
1160
|
+
format: {
|
|
1161
|
+
type: "string",
|
|
1162
|
+
description: "Output format: md | json",
|
|
1163
|
+
default: "md"
|
|
1164
|
+
},
|
|
1165
|
+
watch: {
|
|
1166
|
+
type: "boolean",
|
|
1167
|
+
description: "Watch for changes and rebuild"
|
|
1168
|
+
},
|
|
1169
|
+
silent: {
|
|
1170
|
+
type: "boolean",
|
|
1171
|
+
description: "Suppress non-error output"
|
|
1172
|
+
}
|
|
1173
|
+
},
|
|
1174
|
+
async run({ args }) {
|
|
1175
|
+
const inputPaths = collectInputPaths();
|
|
1176
|
+
if (inputPaths.length === 0) {
|
|
1177
|
+
console.error("Error: No input files or directories specified");
|
|
1178
|
+
console.error("Usage: compmark <files/dirs/globs> [options]");
|
|
1179
|
+
process.exit(1);
|
|
1180
|
+
}
|
|
1181
|
+
const format = args.format;
|
|
1182
|
+
if (format !== "md" && format !== "json") {
|
|
1183
|
+
console.error(`Error: Unknown format "${format}". Use "md" or "json".`);
|
|
1184
|
+
process.exit(1);
|
|
1185
|
+
}
|
|
1186
|
+
const ignorePatterns = args.ignore ? args.ignore.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
1187
|
+
const silent = args.silent ?? false;
|
|
1188
|
+
const joined = args.join ?? false;
|
|
1189
|
+
const outDir = args.out ?? ".";
|
|
1190
|
+
const rebuild = async () => {
|
|
1191
|
+
const filePaths = await discoverFiles(inputPaths, ignorePatterns);
|
|
1192
|
+
if (filePaths.length === 0) {
|
|
1193
|
+
if (!args.watch) {
|
|
1194
|
+
console.error("Error: No .vue files found");
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
}
|
|
1197
|
+
console.warn("Warning: No .vue files found");
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
const summary = processFiles(filePaths, { silent });
|
|
1201
|
+
if (format === "json") writeJSON(summary.files, outDir, joined, silent);
|
|
1202
|
+
else if (joined) writeJoinedMarkdown(summary.files, outDir, silent);
|
|
1203
|
+
else writeIndividualMarkdown(summary.files, outDir, silent);
|
|
1204
|
+
if (!silent) console.log(`✓ ${summary.documented} components documented, ${summary.skipped} skipped, ${summary.errors} errors`);
|
|
1205
|
+
return summary;
|
|
1206
|
+
};
|
|
1207
|
+
const summary = await rebuild();
|
|
1208
|
+
if (args.watch) startWatcher(inputPaths, ignorePatterns, () => {
|
|
1209
|
+
rebuild();
|
|
1210
|
+
});
|
|
1211
|
+
else if (summary && summary.errors > 0) process.exit(1);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
function collectInputPaths() {
|
|
1215
|
+
const argv = process.argv.slice(2);
|
|
1216
|
+
const paths = [];
|
|
1217
|
+
const flagsWithValue = new Set([
|
|
1218
|
+
"--out",
|
|
1219
|
+
"--ignore",
|
|
1220
|
+
"--format"
|
|
1221
|
+
]);
|
|
1222
|
+
let i = 0;
|
|
1223
|
+
while (i < argv.length) {
|
|
1224
|
+
const arg = argv[i];
|
|
1225
|
+
if (arg === "--") {
|
|
1226
|
+
paths.push(...argv.slice(i + 1));
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
if (flagsWithValue.has(arg)) i += 2;
|
|
1230
|
+
else if (arg.startsWith("--") && arg.includes("=")) i++;
|
|
1231
|
+
else if (arg.startsWith("--")) i++;
|
|
1232
|
+
else {
|
|
1233
|
+
paths.push(arg);
|
|
1234
|
+
i++;
|
|
1235
|
+
}
|
|
904
1236
|
}
|
|
905
|
-
|
|
906
|
-
const outFile = `${doc.name}.md`;
|
|
907
|
-
writeFileSync(join(process.cwd(), outFile), md, "utf-8");
|
|
908
|
-
console.log(`Created ${outFile}`);
|
|
909
|
-
} catch (err) {
|
|
910
|
-
const name = abs.split("/").pop() ?? filePath;
|
|
911
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
912
|
-
console.error(`Error: Could not parse ${name}: ${reason}`);
|
|
913
|
-
process.exit(1);
|
|
1237
|
+
return paths;
|
|
914
1238
|
}
|
|
1239
|
+
runMain(main);
|
|
915
1240
|
//#endregion
|
|
916
1241
|
export {};
|
package/dist/index.d.mts
CHANGED
|
@@ -38,20 +38,49 @@ interface ComponentDoc {
|
|
|
38
38
|
name: string;
|
|
39
39
|
description?: string;
|
|
40
40
|
internal?: boolean;
|
|
41
|
+
scriptSetup?: boolean;
|
|
41
42
|
props: PropDoc[];
|
|
42
43
|
emits: EmitDoc[];
|
|
43
44
|
slots?: SlotDoc[];
|
|
44
45
|
exposes?: ExposeDoc[];
|
|
45
46
|
composables?: ComposableDoc[];
|
|
46
47
|
}
|
|
48
|
+
type OutputFormat = "md" | "json";
|
|
49
|
+
interface RunSummary {
|
|
50
|
+
documented: number;
|
|
51
|
+
skipped: number;
|
|
52
|
+
errors: number;
|
|
53
|
+
files: Array<{
|
|
54
|
+
path: string;
|
|
55
|
+
doc: ComponentDoc;
|
|
56
|
+
}>;
|
|
57
|
+
errorDetails: Array<{
|
|
58
|
+
path: string;
|
|
59
|
+
error: string;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
47
62
|
//#endregion
|
|
48
63
|
//#region src/parser.d.ts
|
|
49
64
|
declare function parseSFC(source: string, filename: string, sfcDir?: string): ComponentDoc;
|
|
50
65
|
//#endregion
|
|
51
66
|
//#region src/markdown.d.ts
|
|
52
67
|
declare function generateMarkdown(doc: ComponentDoc): string;
|
|
68
|
+
declare function adjustHeadingLevel(md: string, increment: number): string;
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/discovery.d.ts
|
|
71
|
+
declare function discoverFiles(inputs: string[], ignore?: string[]): Promise<string[]>;
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/runner.d.ts
|
|
74
|
+
declare function processFiles(filePaths: string[], options: {
|
|
75
|
+
silent?: boolean;
|
|
76
|
+
}): RunSummary;
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/type-resolver.d.ts
|
|
79
|
+
declare function resolveImportedPropsType(typeName: string, importMap: Map<string, string>, sfcDir: string): {
|
|
80
|
+
members: Array<any>;
|
|
81
|
+
} | null;
|
|
53
82
|
//#endregion
|
|
54
83
|
//#region src/index.d.ts
|
|
55
84
|
declare function parseComponent(filePath: string): ComponentDoc;
|
|
56
85
|
//#endregion
|
|
57
|
-
export { type ComponentDoc, type ComposableDoc, type ComposableVariable, type EmitDoc, type ExposeDoc, type PropDoc, type SlotDoc, generateMarkdown, parseComponent, parseSFC };
|
|
86
|
+
export { type ComponentDoc, type ComposableDoc, type ComposableVariable, type EmitDoc, type ExposeDoc, type OutputFormat, type PropDoc, type RunSummary, type SlotDoc, adjustHeadingLevel, discoverFiles, generateMarkdown, parseComponent, parseSFC, processFiles, resolveImportedPropsType };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { babelParse, compileScript, parse } from "@vue/compiler-sfc";
|
|
4
|
+
import { glob } from "tinyglobby";
|
|
4
5
|
//#region src/resolver.ts
|
|
5
6
|
function resolveImportPath(importSource, sfcDir) {
|
|
6
7
|
try {
|
|
@@ -27,7 +28,7 @@ function resolveImportPath(importSource, sfcDir) {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
function tryResolveFile(basePath) {
|
|
30
|
-
if (existsSync(basePath) && !isDirectory(basePath)) return basePath;
|
|
31
|
+
if (existsSync(basePath) && !isDirectory$1(basePath)) return basePath;
|
|
31
32
|
for (const ext of [".ts", ".js"]) {
|
|
32
33
|
const candidate = basePath + ext;
|
|
33
34
|
if (existsSync(candidate)) return candidate;
|
|
@@ -38,7 +39,7 @@ function tryResolveFile(basePath) {
|
|
|
38
39
|
}
|
|
39
40
|
return null;
|
|
40
41
|
}
|
|
41
|
-
function isDirectory(filePath) {
|
|
42
|
+
function isDirectory$1(filePath) {
|
|
42
43
|
try {
|
|
43
44
|
return statSync(filePath).isDirectory();
|
|
44
45
|
} catch {
|
|
@@ -264,6 +265,56 @@ function resolveTypeAnnotation(node) {
|
|
|
264
265
|
}
|
|
265
266
|
}
|
|
266
267
|
//#endregion
|
|
268
|
+
//#region src/type-resolver.ts
|
|
269
|
+
function resolveImportedPropsType(typeName, importMap, sfcDir) {
|
|
270
|
+
const source = importMap.get(typeName);
|
|
271
|
+
if (!source) return null;
|
|
272
|
+
const resolvedPath = resolveImportPath(source, sfcDir);
|
|
273
|
+
if (!resolvedPath) return null;
|
|
274
|
+
try {
|
|
275
|
+
return findExportedType(babelParse(readFileSync(resolvedPath, "utf-8"), {
|
|
276
|
+
plugins: ["typescript"],
|
|
277
|
+
sourceType: "module"
|
|
278
|
+
}).program.body, typeName, 0);
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function findExportedType(stmts, typeName, depth) {
|
|
284
|
+
if (depth > 5) return null;
|
|
285
|
+
for (const stmt of stmts) {
|
|
286
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "TSInterfaceDeclaration" && stmt.declaration.id.name === typeName) return resolveInterfaceMembers(stmt.declaration, stmts, depth);
|
|
287
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "TSTypeAliasDeclaration" && stmt.declaration.id.name === typeName && stmt.declaration.typeAnnotation.type === "TSTypeLiteral") return { members: [...stmt.declaration.typeAnnotation.members] };
|
|
288
|
+
}
|
|
289
|
+
if (hasNamedExport(stmts, typeName)) return findTypeInFile(stmts, typeName, depth);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
function findTypeInFile(stmts, typeName, depth) {
|
|
293
|
+
if (depth > 5) return null;
|
|
294
|
+
for (const stmt of stmts) {
|
|
295
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "TSInterfaceDeclaration" && stmt.declaration.id.name === typeName) return resolveInterfaceMembers(stmt.declaration, stmts, depth);
|
|
296
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "TSTypeAliasDeclaration" && stmt.declaration.id.name === typeName && stmt.declaration.typeAnnotation.type === "TSTypeLiteral") return { members: [...stmt.declaration.typeAnnotation.members] };
|
|
297
|
+
if (stmt.type === "TSInterfaceDeclaration" && stmt.id.name === typeName) return resolveInterfaceMembers(stmt, stmts, depth);
|
|
298
|
+
if (stmt.type === "TSTypeAliasDeclaration" && stmt.id.name === typeName && stmt.typeAnnotation.type === "TSTypeLiteral") return { members: [...stmt.typeAnnotation.members] };
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
function resolveInterfaceMembers(decl, stmts, depth) {
|
|
303
|
+
const members = [];
|
|
304
|
+
if (decl.extends) for (const ext of decl.extends) {
|
|
305
|
+
const parentName = ext.expression?.type === "Identifier" ? ext.expression.name : null;
|
|
306
|
+
if (parentName) {
|
|
307
|
+
const parent = findTypeInFile(stmts, parentName, depth + 1);
|
|
308
|
+
if (parent) members.push(...parent.members);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
members.push(...decl.body.body);
|
|
312
|
+
return { members };
|
|
313
|
+
}
|
|
314
|
+
function hasNamedExport(stmts, name) {
|
|
315
|
+
return stmts.some((s) => s.type === "ExportNamedDeclaration" && !s.declaration && s.specifiers.some((spec) => spec.type === "ExportSpecifier" && (spec.local.type === "Identifier" && spec.local.name === name || spec.exported.type === "Identifier" && spec.exported.name === name)));
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
267
318
|
//#region src/parser.ts
|
|
268
319
|
function parseJSDocTags(comments) {
|
|
269
320
|
const result = { description: "" };
|
|
@@ -290,29 +341,55 @@ function parseSFC(source, filename, sfcDir) {
|
|
|
290
341
|
props: [],
|
|
291
342
|
emits: []
|
|
292
343
|
};
|
|
293
|
-
const
|
|
344
|
+
const fullPath = sfcDir ? `${sfcDir}/${filename}` : filename;
|
|
345
|
+
const { descriptor } = parse(source, { filename: fullPath });
|
|
346
|
+
doc.scriptSetup = !!descriptor.scriptSetup;
|
|
294
347
|
if (descriptor.template?.ast) {
|
|
295
348
|
const templateSlots = extractTemplateSlots(descriptor.template.ast);
|
|
296
349
|
if (templateSlots.length > 0) doc.slots = templateSlots;
|
|
297
350
|
}
|
|
298
351
|
if (!descriptor.scriptSetup && !descriptor.script) return doc;
|
|
299
|
-
|
|
352
|
+
let compiled;
|
|
353
|
+
try {
|
|
354
|
+
compiled = compileScript(descriptor, {
|
|
355
|
+
id: fullPath,
|
|
356
|
+
fs: {
|
|
357
|
+
fileExists: (file) => existsSync(file),
|
|
358
|
+
readFile: (file) => {
|
|
359
|
+
try {
|
|
360
|
+
return readFileSync(file, "utf-8");
|
|
361
|
+
} catch {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
} catch {
|
|
368
|
+
return doc;
|
|
369
|
+
}
|
|
300
370
|
const componentJSDoc = extractComponentJSDoc(compiled.scriptSetupAst ?? compiled.scriptAst ?? []);
|
|
301
371
|
doc.description = componentJSDoc.description;
|
|
302
372
|
doc.internal = componentJSDoc.internal;
|
|
303
373
|
const setupAst = compiled.scriptSetupAst;
|
|
304
374
|
if (setupAst) {
|
|
305
375
|
const scriptSource = descriptor.scriptSetup?.content ?? compiled.content;
|
|
376
|
+
const importMap = buildImportMap(setupAst);
|
|
306
377
|
for (const stmt of setupAst) {
|
|
307
378
|
const calls = extractDefineCalls(stmt);
|
|
308
379
|
for (const { callee, args, leadingComments, typeParams, defaultsArg } of calls) if (callee === "defineProps" && args[0]?.type === "ObjectExpression") doc.props = extractProps(args[0], scriptSource);
|
|
309
380
|
else if (callee === "defineProps" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.props = extractTypeProps(typeParams.params[0], defaultsArg, scriptSource);
|
|
310
|
-
else if (callee === "
|
|
381
|
+
else if (callee === "defineProps" && typeParams?.params[0]?.type === "TSTypeReference") {
|
|
382
|
+
const typeName = typeParams.params[0].typeName?.name;
|
|
383
|
+
if (typeName && sfcDir) {
|
|
384
|
+
const resolved = resolveImportedPropsType(typeName, importMap, sfcDir);
|
|
385
|
+
if (resolved) doc.props = extractTypeProps(resolved, defaultsArg, scriptSource);
|
|
386
|
+
}
|
|
387
|
+
} else if (callee === "defineEmits" && args[0]?.type === "ArrayExpression") doc.emits = extractEmits(args[0], leadingComments);
|
|
311
388
|
else if (callee === "defineEmits" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.emits = extractTypeEmits(typeParams.params[0]);
|
|
312
389
|
else if (callee === "defineSlots" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.slots = extractTypeSlots(typeParams.params[0]);
|
|
313
390
|
else if (callee === "defineExpose" && args[0]?.type === "ObjectExpression") doc.exposes = extractExposes(args[0], scriptSource);
|
|
314
391
|
}
|
|
315
|
-
doc.composables = extractComposables(setupAst,
|
|
392
|
+
doc.composables = extractComposables(setupAst, importMap, sfcDir);
|
|
316
393
|
}
|
|
317
394
|
const scriptAst = compiled.scriptAst;
|
|
318
395
|
if (scriptAst && doc.props.length === 0 && doc.emits.length === 0) {
|
|
@@ -784,6 +861,7 @@ function escHtml(value) {
|
|
|
784
861
|
function generateMarkdown(doc) {
|
|
785
862
|
const sections = [`# ${doc.name}`];
|
|
786
863
|
if (doc.description) sections.push("", doc.description);
|
|
864
|
+
if (doc.scriptSetup) sections.push("", "**Note:** Uses `<script setup>` syntax.");
|
|
787
865
|
const hasProps = doc.props.length > 0;
|
|
788
866
|
const hasEmits = doc.emits.length > 0;
|
|
789
867
|
const hasSlots = (doc.slots?.length ?? 0) > 0;
|
|
@@ -872,6 +950,87 @@ function generateMarkdown(doc) {
|
|
|
872
950
|
}
|
|
873
951
|
return sections.join("\n") + "\n";
|
|
874
952
|
}
|
|
953
|
+
function adjustHeadingLevel(md, increment) {
|
|
954
|
+
return md.replace(/^(#{1,6})\s/gm, (_, hashes) => {
|
|
955
|
+
const newLevel = Math.min(hashes.length + increment, 6);
|
|
956
|
+
return "#".repeat(newLevel) + " ";
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
//#endregion
|
|
960
|
+
//#region src/discovery.ts
|
|
961
|
+
async function discoverFiles(inputs, ignore) {
|
|
962
|
+
const baseIgnore = ["**/node_modules/**"];
|
|
963
|
+
const userIgnore = (ignore ?? []).map(normalizeIgnorePattern);
|
|
964
|
+
const allIgnore = [...baseIgnore, ...userIgnore];
|
|
965
|
+
const found = /* @__PURE__ */ new Set();
|
|
966
|
+
for (const input of inputs) {
|
|
967
|
+
const resolved = resolve(input);
|
|
968
|
+
if (input.endsWith(".vue") && existsSync(resolved)) found.add(resolved);
|
|
969
|
+
else if (isDirectory(resolved)) {
|
|
970
|
+
const files = await glob("**/*.vue", {
|
|
971
|
+
cwd: resolved,
|
|
972
|
+
absolute: true,
|
|
973
|
+
ignore: allIgnore
|
|
974
|
+
});
|
|
975
|
+
for (const f of files) found.add(f);
|
|
976
|
+
} else {
|
|
977
|
+
const files = await glob(input, {
|
|
978
|
+
absolute: true,
|
|
979
|
+
ignore: allIgnore
|
|
980
|
+
});
|
|
981
|
+
for (const f of files) found.add(f);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return [...found].sort();
|
|
985
|
+
}
|
|
986
|
+
function isDirectory(path) {
|
|
987
|
+
try {
|
|
988
|
+
return statSync(path).isDirectory();
|
|
989
|
+
} catch {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function normalizeIgnorePattern(pattern) {
|
|
994
|
+
if (pattern.includes("*") || pattern.includes("/")) return pattern;
|
|
995
|
+
return `**/${pattern}/**`;
|
|
996
|
+
}
|
|
997
|
+
//#endregion
|
|
998
|
+
//#region src/runner.ts
|
|
999
|
+
function processFiles(filePaths, options) {
|
|
1000
|
+
const summary = {
|
|
1001
|
+
documented: 0,
|
|
1002
|
+
skipped: 0,
|
|
1003
|
+
errors: 0,
|
|
1004
|
+
files: [],
|
|
1005
|
+
errorDetails: []
|
|
1006
|
+
};
|
|
1007
|
+
for (const filePath of filePaths) try {
|
|
1008
|
+
const doc = parseComponent(filePath);
|
|
1009
|
+
if (doc.internal) {
|
|
1010
|
+
summary.skipped++;
|
|
1011
|
+
if (!options.silent) {
|
|
1012
|
+
const name = filePath.split("/").pop() ?? filePath;
|
|
1013
|
+
console.log(` Skipped ${name} (marked @internal)`);
|
|
1014
|
+
}
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
summary.documented++;
|
|
1018
|
+
summary.files.push({
|
|
1019
|
+
path: filePath,
|
|
1020
|
+
doc
|
|
1021
|
+
});
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
summary.errors++;
|
|
1024
|
+
const name = filePath.split("/").pop() ?? filePath;
|
|
1025
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1026
|
+
summary.errorDetails.push({
|
|
1027
|
+
path: filePath,
|
|
1028
|
+
error: message
|
|
1029
|
+
});
|
|
1030
|
+
if (!options.silent) console.warn(` Warning: Could not parse ${name}: ${message}`);
|
|
1031
|
+
}
|
|
1032
|
+
return summary;
|
|
1033
|
+
}
|
|
875
1034
|
//#endregion
|
|
876
1035
|
//#region src/index.ts
|
|
877
1036
|
function parseComponent(filePath) {
|
|
@@ -879,4 +1038,4 @@ function parseComponent(filePath) {
|
|
|
879
1038
|
return parseSFC(readFileSync(abs, "utf-8"), abs.split("/").pop() ?? "Unknown.vue", abs.substring(0, abs.lastIndexOf("/")));
|
|
880
1039
|
}
|
|
881
1040
|
//#endregion
|
|
882
|
-
export { generateMarkdown, parseComponent, parseSFC };
|
|
1041
|
+
export { adjustHeadingLevel, discoverFiles, generateMarkdown, parseComponent, parseSFC, processFiles, resolveImportedPropsType };
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compmark-vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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": "./dist/cli.mjs"
|
|
8
|
+
"compmark": "./dist/cli.mjs",
|
|
9
|
+
"compmark-vue": "./dist/cli.mjs"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"dist"
|
|
@@ -28,7 +29,9 @@
|
|
|
28
29
|
"prepare": "husky"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
|
-
"@vue/compiler-sfc": "^3.5.0"
|
|
32
|
+
"@vue/compiler-sfc": "^3.5.0",
|
|
33
|
+
"citty": "^0.2.1",
|
|
34
|
+
"tinyglobby": "^0.2.15"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
34
37
|
"@babel/types": "latest",
|
|
@@ -51,5 +54,8 @@
|
|
|
51
54
|
"oxfmt"
|
|
52
55
|
]
|
|
53
56
|
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=20"
|
|
59
|
+
},
|
|
54
60
|
"packageManager": "pnpm@10.29.3"
|
|
55
61
|
}
|