compmark-vue 0.2.4 → 0.2.6

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 CHANGED
@@ -17,25 +17,64 @@ npx compmark-vue ./src/components/Button.vue
17
17
 
18
18
  This parses the component and creates `Button.md` in your current directory.
19
19
 
20
- ### Example Output
20
+ ## Features
21
21
 
22
- Given a component like:
22
+ - [Props](#props) runtime and TypeScript generic syntax
23
+ - [Emits](#emits) — array, TypeScript property, and call signature syntax
24
+ - [Slots](#slots) — `defineSlots` with typed bindings, template `<slot>` fallback
25
+ - [Expose](#expose) — `defineExpose` with JSDoc descriptions
26
+ - [Composables](#composables) — auto-detects `useX()` calls in `<script setup>`
27
+ - [JSDoc tags](#jsdoc-tags) — `@deprecated`, `@since`, `@example`, `@see`, `@default`
28
+ - [`@internal`](#internal-components) — exclude components from output
29
+ - [Options API](#options-api) — `export default { props, emits }` support
30
+ - Empty sections are skipped cleanly — no placeholder noise
31
+
32
+ ## Examples
33
+
34
+ ### Props
35
+
36
+ Runtime syntax, TypeScript generics, and `withDefaults` are all supported:
23
37
 
24
38
  ```vue
25
- <script setup>
26
- /**
27
- * @emit submit Emitted on form submit
28
- * @emit cancel Emitted on cancel
29
- */
30
- const emit = defineEmits(["submit", "cancel"]);
39
+ <script setup lang="ts">
40
+ const props = withDefaults(
41
+ defineProps<{
42
+ /** The label text */
43
+ label: string;
44
+ /** Visual theme */
45
+ theme?: "filled" | "outline";
46
+ disabled?: boolean;
47
+ }>(),
48
+ {
49
+ theme: "filled",
50
+ disabled: false,
51
+ },
52
+ );
53
+ </script>
54
+ ```
31
55
 
32
- const props = defineProps({
56
+ Output:
57
+
58
+ ```md
59
+ ## Props
60
+
61
+ | Name | Type | Required | Default | Description |
62
+ | -------- | --------------------- | -------- | ---------- | -------------- |
63
+ | label | string | Yes | - | The label text |
64
+ | theme | 'filled' \| 'outline' | No | `"filled"` | Visual theme |
65
+ | disabled | boolean | No | `false` | - |
66
+ ```
67
+
68
+ Runtime object syntax is also supported:
69
+
70
+ ```vue
71
+ <script setup>
72
+ defineProps({
33
73
  /** Title of the dialog */
34
74
  title: {
35
75
  type: String,
36
76
  required: true,
37
77
  },
38
- /** Whether the dialog is visible */
39
78
  visible: {
40
79
  type: Boolean,
41
80
  default: false,
@@ -44,24 +83,224 @@ const props = defineProps({
44
83
  </script>
45
84
  ```
46
85
 
47
- compmark-vue generates:
86
+ ### Emits
87
+
88
+ TypeScript generic syntax with payloads:
89
+
90
+ ```vue
91
+ <script setup lang="ts">
92
+ const emit = defineEmits<{
93
+ /** Emitted on save */
94
+ save: [data: Record<string, unknown>];
95
+ /** Emitted on cancel */
96
+ cancel: [];
97
+ }>();
98
+ </script>
99
+ ```
100
+
101
+ Output:
102
+
103
+ ```md
104
+ ## Emits
105
+
106
+ | Name | Payload | Description |
107
+ | ------ | ----------------------------- | ----------------- |
108
+ | save | data: Record<string, unknown> | Emitted on save |
109
+ | cancel | - | Emitted on cancel |
110
+ ```
111
+
112
+ Call signature syntax is also supported:
113
+
114
+ ```vue
115
+ <script setup lang="ts">
116
+ defineEmits<{
117
+ (e: "click", payload: MouseEvent): void;
118
+ (e: "submit"): void;
119
+ }>();
120
+ </script>
121
+ ```
122
+
123
+ Array syntax works too: `defineEmits(["click", "submit"])`.
124
+
125
+ ### Slots
126
+
127
+ `defineSlots` provides typed bindings:
128
+
129
+ ```vue
130
+ <script setup lang="ts">
131
+ defineSlots<{
132
+ /** Main content */
133
+ default(props: { msg: string }): any;
134
+ /** Header area */
135
+ header(props: { title: string; count: number }): any;
136
+ }>();
137
+ </script>
138
+ ```
139
+
140
+ Output:
48
141
 
49
142
  ```md
50
- # Dialog
143
+ ## Slots
144
+
145
+ | Name | Bindings | Description |
146
+ | ------- | ---------------------------- | ------------ |
147
+ | default | msg: string | Main content |
148
+ | header | title: string, count: number | Header area |
149
+ ```
150
+
151
+ If `defineSlots` is not used, slots are extracted from template `<slot>` elements as a fallback:
152
+
153
+ ```vue
154
+ <template>
155
+ <div>
156
+ <slot />
157
+ <slot name="header" :title="title" />
158
+ <slot name="footer" />
159
+ </div>
160
+ </template>
161
+ ```
162
+
163
+ ### Expose
164
+
165
+ ```vue
166
+ <script setup lang="ts">
167
+ defineExpose({
168
+ /** Focus the component */
169
+ focus,
170
+ /** Reset the component state */
171
+ reset,
172
+ });
173
+ </script>
174
+ ```
51
175
 
176
+ Output:
177
+
178
+ ```md
179
+ ## Exposed
180
+
181
+ | Name | Type | Description |
182
+ | ----- | ------- | ------------------------- |
183
+ | focus | unknown | Focus the component |
184
+ | reset | unknown | Reset the component state |
185
+ ```
186
+
187
+ ### Composables
188
+
189
+ Any `useX()` calls in `<script setup>` are automatically detected:
190
+
191
+ ```vue
192
+ <script setup lang="ts">
193
+ import { useRouter } from "vue-router";
194
+ import { useMouse } from "@vueuse/core";
195
+
196
+ const router = useRouter();
197
+ const { x, y } = useMouse();
198
+ </script>
199
+ ```
200
+
201
+ Output:
202
+
203
+ ```md
204
+ ## Composables Used
205
+
206
+ - `useRouter`
207
+ - `useMouse`
208
+ ```
209
+
210
+ ### JSDoc Tags
211
+
212
+ Props support `@deprecated`, `@since`, `@example`, and `@see`:
213
+
214
+ ```vue
215
+ <script setup lang="ts">
216
+ defineProps<{
217
+ /**
218
+ * The label text
219
+ * @deprecated Use `text` instead
220
+ * @since 1.0.0
221
+ * @example "Hello World"
222
+ * @see https://example.com/docs
223
+ */
224
+ label: string;
225
+ }>();
226
+ </script>
227
+ ```
228
+
229
+ Output:
230
+
231
+ ````md
52
232
  ## Props
53
233
 
54
- | Name | Type | Required | Default | Description |
55
- | ------- | ------- | -------- | ------- | ----------------------------- |
56
- | title | String | Yes | - | Title of the dialog |
57
- | visible | Boolean | No | `false` | Whether the dialog is visible |
234
+ | Name | Type | Required | Default | Description |
235
+ | ----- | ------ | -------- | ------- | ----------------------------------------------------------------------------------------------- |
236
+ | label | string | Yes | - | The label text **Deprecated**: Use `text` instead _(since 1.0.0)_ See: https://example.com/docs |
237
+
238
+ **`label` example:**
239
+
240
+ ```
241
+ "Hello World"
242
+ ```
243
+ ````
244
+
245
+ ### Internal Components
246
+
247
+ Mark a component with `@internal` to skip it during generation:
248
+
249
+ ```vue
250
+ <script setup lang="ts">
251
+ /**
252
+ * @internal
253
+ */
254
+ defineProps<{
255
+ value: string;
256
+ }>();
257
+ </script>
258
+ ```
259
+
260
+ ```sh
261
+ $ compmark InternalHelper.vue
262
+ Skipped InternalHelper.vue (marked @internal)
263
+ ```
264
+
265
+ ### Options API
266
+
267
+ Components using `export default {}` are supported:
268
+
269
+ ```vue
270
+ <script>
271
+ export default {
272
+ props: {
273
+ /** The title text */
274
+ title: {
275
+ type: String,
276
+ required: true,
277
+ },
278
+ count: {
279
+ type: Number,
280
+ default: 10,
281
+ },
282
+ },
283
+ emits: ["click", "update"],
284
+ };
285
+ </script>
286
+ ```
287
+
288
+ Output:
289
+
290
+ ```md
291
+ ## Props
292
+
293
+ | Name | Type | Required | Default | Description |
294
+ | ----- | ------ | -------- | ------- | -------------- |
295
+ | title | String | Yes | - | The title text |
296
+ | count | Number | No | `10` | - |
58
297
 
59
298
  ## Emits
60
299
 
61
- | Name | Description |
62
- | ------ | ---------------------- |
63
- | submit | Emitted on form submit |
64
- | cancel | Emitted on cancel |
300
+ | Name | Description |
301
+ | ------ | ----------- |
302
+ | click | - |
303
+ | update | - |
65
304
  ```
66
305
 
67
306
  ## Programmatic API
@@ -86,14 +325,6 @@ const doc = parseSFC(source, "Button.vue");
86
325
  const md = generateMarkdown(doc);
87
326
  ```
88
327
 
89
- ## Supported Syntax
90
-
91
- - `defineProps({ ... })` — shorthand (`String`), array type (`[String, Number]`), and full object syntax
92
- - `defineEmits([...])` — array syntax
93
- - JSDoc comments on props and emits (`/** ... */`)
94
- - `const props = defineProps(...)` variable assignment pattern
95
- - Default value extraction (string, number, boolean literals, arrow functions)
96
-
97
328
  ## Development
98
329
 
99
330
  <details>
package/dist/cli.mjs CHANGED
@@ -1,7 +1,270 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
- import { join, resolve } from "node:path";
4
- import { compileScript, parse } from "@vue/compiler-sfc";
2
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { babelParse, compileScript, parse } from "@vue/compiler-sfc";
5
+ //#region src/resolver.ts
6
+ function resolveImportPath(importSource, sfcDir) {
7
+ try {
8
+ if (!importSource.startsWith(".") && !importSource.startsWith("@/") && !importSource.startsWith("~/")) return null;
9
+ if (importSource.startsWith("./") || importSource.startsWith("../")) return tryResolveFile(resolve(sfcDir, importSource));
10
+ const tsconfig = findTsConfig(sfcDir);
11
+ if (!tsconfig) return null;
12
+ const { paths, baseUrl } = readTsConfigPaths(tsconfig);
13
+ if (!paths) return null;
14
+ const configDir = dirname(tsconfig);
15
+ const resolvedBaseUrl = baseUrl ? resolve(configDir, baseUrl) : configDir;
16
+ for (const [pattern, targets] of Object.entries(paths)) {
17
+ const prefix = pattern.replace(/\*$/, "");
18
+ if (!importSource.startsWith(prefix)) continue;
19
+ const remainder = importSource.slice(prefix.length);
20
+ for (const target of targets) {
21
+ const result = tryResolveFile(resolve(resolvedBaseUrl, target.replace(/\*$/, "") + remainder));
22
+ if (result) return result;
23
+ }
24
+ }
25
+ return null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ function tryResolveFile(basePath) {
31
+ if (existsSync(basePath) && !isDirectory(basePath)) return basePath;
32
+ for (const ext of [".ts", ".js"]) {
33
+ const candidate = basePath + ext;
34
+ if (existsSync(candidate)) return candidate;
35
+ }
36
+ for (const ext of ["/index.ts", "/index.js"]) {
37
+ const candidate = basePath + ext;
38
+ if (existsSync(candidate)) return candidate;
39
+ }
40
+ return null;
41
+ }
42
+ function isDirectory(filePath) {
43
+ try {
44
+ return statSync(filePath).isDirectory();
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+ function findTsConfig(startDir) {
50
+ let dir = resolve(startDir);
51
+ const root = resolve("/");
52
+ while (dir !== root) {
53
+ const tsconfig = join(dir, "tsconfig.json");
54
+ if (existsSync(tsconfig)) return tsconfig;
55
+ const jsconfig = join(dir, "jsconfig.json");
56
+ if (existsSync(jsconfig)) return jsconfig;
57
+ const parent = dirname(dir);
58
+ if (parent === dir) break;
59
+ dir = parent;
60
+ }
61
+ return null;
62
+ }
63
+ function readTsConfigPaths(configPath) {
64
+ try {
65
+ const content = JSON.parse(readFileSync(configPath, "utf-8"));
66
+ let paths = content.compilerOptions?.paths ?? null;
67
+ let baseUrl = content.compilerOptions?.baseUrl;
68
+ if (content.extends) {
69
+ const parentPath = resolve(dirname(configPath), content.extends);
70
+ const parentConfigFile = parentPath.endsWith(".json") ? parentPath : parentPath + ".json";
71
+ if (existsSync(parentConfigFile)) try {
72
+ const parentContent = JSON.parse(readFileSync(parentConfigFile, "utf-8"));
73
+ const parentPaths = parentContent.compilerOptions?.paths;
74
+ const parentBaseUrl = parentContent.compilerOptions?.baseUrl;
75
+ if (!paths && parentPaths) paths = parentPaths;
76
+ if (!baseUrl && parentBaseUrl) baseUrl = parentBaseUrl;
77
+ } catch {}
78
+ }
79
+ return {
80
+ paths,
81
+ baseUrl
82
+ };
83
+ } catch {
84
+ return {
85
+ paths: null,
86
+ baseUrl: void 0
87
+ };
88
+ }
89
+ }
90
+ function resolveComposableTypes(filePath, exportName, variableNames) {
91
+ try {
92
+ const funcNode = findExportedFunction(babelParse(readFileSync(filePath, "utf-8"), {
93
+ plugins: ["typescript", "jsx"],
94
+ sourceType: "module"
95
+ }).program.body, exportName);
96
+ if (!funcNode) return /* @__PURE__ */ new Map();
97
+ const body = getFunctionBody(funcNode);
98
+ if (!body) return /* @__PURE__ */ new Map();
99
+ const returnProps = findReturnProperties(body);
100
+ if (!returnProps) return /* @__PURE__ */ new Map();
101
+ const result = /* @__PURE__ */ new Map();
102
+ const nameSet = new Set(variableNames);
103
+ for (const prop of returnProps) {
104
+ let propName = null;
105
+ if (prop.type === "ObjectProperty") propName = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : null;
106
+ else if (prop.type === "ObjectMethod") {
107
+ propName = prop.key.type === "Identifier" ? prop.key.name : null;
108
+ if (propName && nameSet.has(propName)) result.set(propName, inferFunctionSignature(prop));
109
+ continue;
110
+ } else if (prop.type === "SpreadElement") continue;
111
+ if (!propName || !nameSet.has(propName)) continue;
112
+ if (prop.type === "ObjectProperty" && prop.shorthand) {
113
+ const type = traceVariableType(propName, body);
114
+ result.set(propName, type);
115
+ } else if (prop.type === "ObjectProperty") {
116
+ const type = inferType(prop.value);
117
+ result.set(propName, type);
118
+ }
119
+ }
120
+ return result;
121
+ } catch {
122
+ return /* @__PURE__ */ new Map();
123
+ }
124
+ }
125
+ function findExportedFunction(stmts, exportName) {
126
+ for (const stmt of stmts) {
127
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "FunctionDeclaration" && stmt.declaration.id?.name === exportName) return stmt.declaration;
128
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "VariableDeclaration") {
129
+ for (const decl of stmt.declaration.declarations) if (decl.id.type === "Identifier" && decl.id.name === exportName && decl.init && (decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")) return decl.init;
130
+ }
131
+ if (stmt.type === "ExportDefaultDeclaration" && stmt.declaration.type === "FunctionDeclaration" && stmt.declaration.id?.name === exportName) return stmt.declaration;
132
+ if (stmt.type === "ExportDefaultDeclaration" && stmt.declaration.type === "FunctionDeclaration" && !stmt.declaration.id) return stmt.declaration;
133
+ if (stmt.type === "ExportDefaultDeclaration" && (stmt.declaration.type === "ArrowFunctionExpression" || stmt.declaration.type === "FunctionExpression")) return stmt.declaration;
134
+ if (stmt.type === "FunctionDeclaration" && stmt.id?.name === exportName) {
135
+ if (stmts.some((s) => s.type === "ExportNamedDeclaration" && !s.declaration && s.specifiers.some((spec) => spec.type === "ExportSpecifier" && (spec.local.type === "Identifier" && spec.local.name === exportName || spec.exported.type === "Identifier" && spec.exported.name === exportName)))) return stmt;
136
+ }
137
+ if (stmt.type === "VariableDeclaration") {
138
+ for (const decl of stmt.declarations) if (decl.id.type === "Identifier" && decl.id.name === exportName && decl.init && (decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")) {
139
+ if (stmts.some((s) => s.type === "ExportNamedDeclaration" && !s.declaration && s.specifiers.some((spec) => spec.type === "ExportSpecifier" && (spec.local.type === "Identifier" && spec.local.name === exportName || spec.exported.type === "Identifier" && spec.exported.name === exportName)))) return decl.init;
140
+ }
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+ function getFunctionBody(node) {
146
+ if (node.body.type === "BlockStatement") return node.body.body;
147
+ return null;
148
+ }
149
+ function findReturnProperties(body) {
150
+ for (let i = body.length - 1; i >= 0; i--) {
151
+ const stmt = body[i];
152
+ if (stmt.type === "ReturnStatement" && stmt.argument?.type === "ObjectExpression") return stmt.argument.properties;
153
+ }
154
+ return null;
155
+ }
156
+ function traceVariableType(name, body) {
157
+ for (let i = body.length - 1; i >= 0; i--) {
158
+ const stmt = body[i];
159
+ if (stmt.type === "FunctionDeclaration" && stmt.id?.name === name) return inferFunctionSignature(stmt);
160
+ if (stmt.type === "VariableDeclaration") {
161
+ for (const decl of stmt.declarations) if (decl.id.type === "Identifier" && decl.id.name === name && decl.init) return inferType(decl.init);
162
+ }
163
+ }
164
+ return "unknown";
165
+ }
166
+ function inferType(node) {
167
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "ref") {
168
+ const typeParams = node.typeParameters;
169
+ if (typeParams?.params?.length > 0) return `Ref<${resolveTypeAnnotation(typeParams.params[0])}>`;
170
+ const arg = node.arguments[0];
171
+ if (!arg) return "Ref<unknown>";
172
+ return `Ref<${inferLiteralType(arg)}>`;
173
+ }
174
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "computed") return "ComputedRef";
175
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "reactive") return "Object";
176
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") return inferFunctionSignature(node);
177
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && /^use[A-Z]/.test(node.callee.name)) return "unknown";
178
+ return inferLiteralType(node);
179
+ }
180
+ function inferLiteralType(node) {
181
+ switch (node.type) {
182
+ case "NumericLiteral": return "number";
183
+ case "StringLiteral": return "string";
184
+ case "BooleanLiteral": return "boolean";
185
+ case "NullLiteral": return "null";
186
+ case "TemplateLiteral": return "string";
187
+ case "ArrayExpression": return "Array";
188
+ case "ObjectExpression": return "Object";
189
+ default: return "unknown";
190
+ }
191
+ }
192
+ function inferFunctionSignature(node) {
193
+ return `(${extractParams(node.params ?? [])}) => ${extractReturnType(node)}`;
194
+ }
195
+ function extractParams(params) {
196
+ return params.map((param) => {
197
+ if (param.type === "Identifier") {
198
+ const annotation = param.typeAnnotation?.typeAnnotation;
199
+ if (annotation) return `${param.name}: ${resolveTypeAnnotation(annotation)}`;
200
+ return param.name;
201
+ }
202
+ if (param.type === "AssignmentPattern") {
203
+ const left = param.left;
204
+ if (left.type === "Identifier") {
205
+ const annotation = left.typeAnnotation?.typeAnnotation;
206
+ if (annotation) return `${left.name}: ${resolveTypeAnnotation(annotation)}`;
207
+ return left.name;
208
+ }
209
+ return "arg";
210
+ }
211
+ if (param.type === "RestElement") {
212
+ const arg = param.argument;
213
+ if (arg.type === "Identifier") {
214
+ const annotation = arg.typeAnnotation?.typeAnnotation;
215
+ if (annotation) return `...${arg.name}: ${resolveTypeAnnotation(annotation)}`;
216
+ return `...${arg.name}`;
217
+ }
218
+ return "...args";
219
+ }
220
+ if (param.type === "ObjectPattern") return "options";
221
+ if (param.type === "ArrayPattern") return "args";
222
+ return "arg";
223
+ }).join(", ");
224
+ }
225
+ function extractReturnType(node) {
226
+ const annotation = node.returnType?.typeAnnotation ?? node.typeAnnotation?.typeAnnotation;
227
+ let baseType;
228
+ if (annotation) baseType = resolveTypeAnnotation(annotation);
229
+ else baseType = "void";
230
+ if (node.async && baseType !== "void") return `Promise<${baseType}>`;
231
+ if (node.async) return "Promise<void>";
232
+ return baseType;
233
+ }
234
+ function resolveTypeAnnotation(node) {
235
+ if (!node) return "unknown";
236
+ switch (node.type) {
237
+ case "TSStringKeyword": return "string";
238
+ case "TSNumberKeyword": return "number";
239
+ case "TSBooleanKeyword": return "boolean";
240
+ case "TSVoidKeyword": return "void";
241
+ case "TSAnyKeyword": return "any";
242
+ case "TSNullKeyword": return "null";
243
+ case "TSUndefinedKeyword": return "undefined";
244
+ case "TSObjectKeyword": return "object";
245
+ case "TSNeverKeyword": return "never";
246
+ case "TSUnknownKeyword": return "unknown";
247
+ case "TSTypeReference": {
248
+ const name = node.typeName?.type === "Identifier" ? node.typeName.name : node.typeName?.type === "TSQualifiedName" ? `${node.typeName.left?.name ?? ""}.${node.typeName.right?.name ?? ""}` : "unknown";
249
+ if (node.typeParameters?.params?.length > 0) return `${name}<${node.typeParameters.params.map((p) => resolveTypeAnnotation(p)).join(", ")}>`;
250
+ return name;
251
+ }
252
+ case "TSUnionType": return node.types.map((t) => resolveTypeAnnotation(t)).join(" | ");
253
+ case "TSIntersectionType": return node.types.map((t) => resolveTypeAnnotation(t)).join(" & ");
254
+ case "TSArrayType": return `${resolveTypeAnnotation(node.elementType)}[]`;
255
+ case "TSLiteralType":
256
+ if (node.literal.type === "StringLiteral") return `'${node.literal.value}'`;
257
+ if (node.literal.type === "NumericLiteral") return String(node.literal.value);
258
+ if (node.literal.type === "BooleanLiteral") return String(node.literal.value);
259
+ return "unknown";
260
+ case "TSFunctionType": return "Function";
261
+ case "TSTupleType": return `[${(node.elementTypes ?? []).map((t) => resolveTypeAnnotation(t)).join(", ")}]`;
262
+ case "TSParenthesizedType": return resolveTypeAnnotation(node.typeAnnotation);
263
+ case "TSTypeLiteral": return "object";
264
+ default: return "unknown";
265
+ }
266
+ }
267
+ //#endregion
5
268
  //#region src/parser.ts
6
269
  function parseJSDocTags(comments) {
7
270
  const result = { description: "" };
@@ -22,7 +285,7 @@ function parseJSDocTags(comments) {
22
285
  result.description = descLines.join(" ");
23
286
  return result;
24
287
  }
25
- function parseSFC(source, filename) {
288
+ function parseSFC(source, filename, sfcDir) {
26
289
  const doc = {
27
290
  name: filename.replace(/\.vue$/, "").split("/").pop() ?? "Unknown",
28
291
  props: [],
@@ -50,7 +313,7 @@ function parseSFC(source, filename) {
50
313
  else if (callee === "defineSlots" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.slots = extractTypeSlots(typeParams.params[0]);
51
314
  else if (callee === "defineExpose" && args[0]?.type === "ObjectExpression") doc.exposes = extractExposes(args[0], scriptSource);
52
315
  }
53
- doc.composables = extractComposables(setupAst);
316
+ doc.composables = extractComposables(setupAst, buildImportMap(setupAst), sfcDir);
54
317
  }
55
318
  const scriptAst = compiled.scriptAst;
56
319
  if (scriptAst && doc.props.length === 0 && doc.emits.length === 0) {
@@ -331,26 +594,94 @@ function extractExposes(obj, _source) {
331
594
  }
332
595
  return exposes;
333
596
  }
334
- function extractComposables(ast) {
597
+ function buildImportMap(ast) {
598
+ const map = /* @__PURE__ */ new Map();
599
+ for (const stmt of ast) if (stmt.type === "ImportDeclaration") {
600
+ for (const spec of stmt.specifiers ?? []) if (spec.type === "ImportSpecifier" || spec.type === "ImportDefaultSpecifier") map.set(spec.local.name, stmt.source.value);
601
+ }
602
+ return map;
603
+ }
604
+ function extractVariablesFromPattern(decl) {
605
+ const id = decl.id;
606
+ if (!id) return [];
607
+ if (id.type === "Identifier") {
608
+ const v = { name: id.name };
609
+ if (id.typeAnnotation?.typeAnnotation) v.type = resolveTypeString(id.typeAnnotation.typeAnnotation);
610
+ return [v];
611
+ }
612
+ if (id.type === "ObjectPattern") {
613
+ const vars = [];
614
+ const typeAnnotation = id.typeAnnotation?.typeAnnotation;
615
+ const typeMembers = typeAnnotation?.type === "TSTypeLiteral" ? typeAnnotation.members : null;
616
+ for (const prop of id.properties) if (prop.type === "RestElement") {
617
+ const name = prop.argument?.name ?? "rest";
618
+ vars.push({ name });
619
+ } else if (prop.type === "ObjectProperty") {
620
+ const name = prop.value?.type === "Identifier" ? prop.value.name : prop.value?.type === "AssignmentPattern" && prop.value.left?.type === "Identifier" ? prop.value.left.name : prop.key?.type === "Identifier" ? prop.key.name : "";
621
+ if (!name) continue;
622
+ const v = { name };
623
+ if (typeMembers) {
624
+ const keyName = prop.key?.type === "Identifier" ? prop.key.name : "";
625
+ for (const member of typeMembers) if (member.type === "TSPropertySignature" && member.key?.type === "Identifier" && member.key.name === keyName && member.typeAnnotation?.typeAnnotation) {
626
+ v.type = resolveTypeString(member.typeAnnotation.typeAnnotation);
627
+ break;
628
+ }
629
+ }
630
+ vars.push(v);
631
+ }
632
+ return vars;
633
+ }
634
+ if (id.type === "ArrayPattern") {
635
+ const vars = [];
636
+ for (const el of id.elements) {
637
+ if (!el) continue;
638
+ if (el.type === "Identifier") vars.push({ name: el.name });
639
+ else if (el.type === "RestElement" && el.argument?.type === "Identifier") vars.push({ name: el.argument.name });
640
+ else if (el.type === "AssignmentPattern" && el.left?.type === "Identifier") vars.push({ name: el.left.name });
641
+ }
642
+ return vars;
643
+ }
644
+ return [];
645
+ }
646
+ function extractComposables(ast, importMap, sfcDir) {
335
647
  const seen = /* @__PURE__ */ new Set();
336
648
  const composables = [];
337
649
  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 });
650
+ if (stmt.type === "ExpressionStatement" && stmt.expression.type === "CallExpression" && stmt.expression.callee.type === "Identifier" && /^use[A-Z]/.test(stmt.expression.callee.name)) {
651
+ const name = stmt.expression.callee.name;
652
+ if (!seen.has(name)) {
653
+ seen.add(name);
654
+ composables.push({
655
+ name,
656
+ source: importMap.get(name),
657
+ variables: []
658
+ });
659
+ }
660
+ }
661
+ if (stmt.type === "VariableDeclaration") {
662
+ for (const decl of stmt.declarations) if (decl.init?.type === "CallExpression" && decl.init.callee.type === "Identifier" && /^use[A-Z]/.test(decl.init.callee.name)) {
663
+ const name = decl.init.callee.name;
664
+ if (seen.has(name)) continue;
665
+ seen.add(name);
666
+ const variables = extractVariablesFromPattern(decl);
667
+ const source = importMap.get(name);
668
+ if (variables.some((v) => !v.type) && sfcDir && source) {
669
+ const resolvedPath = resolveImportPath(source, sfcDir);
670
+ if (resolvedPath) {
671
+ const typeMap = resolveComposableTypes(resolvedPath, name, variables.filter((v) => !v.type).map((v) => v.name));
672
+ for (const v of variables) if (!v.type && typeMap.has(v.name)) v.type = typeMap.get(v.name);
673
+ }
674
+ }
675
+ composables.push({
676
+ name,
677
+ source,
678
+ variables
679
+ });
680
+ }
342
681
  }
343
682
  }
344
683
  return composables;
345
684
  }
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
685
  function extractTemplateSlots(templateAst) {
355
686
  const slots = [];
356
687
  walkTemplate(templateAst.children ?? [], slots);
@@ -448,6 +779,9 @@ function stringifyDefault(node, source) {
448
779
  function esc(value) {
449
780
  return value.replaceAll("|", "\\|");
450
781
  }
782
+ function escHtml(value) {
783
+ return value.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
784
+ }
451
785
  function generateMarkdown(doc) {
452
786
  const sections = [`# ${doc.name}`];
453
787
  if (doc.description) sections.push("", doc.description);
@@ -518,8 +852,24 @@ function generateMarkdown(doc) {
518
852
  }
519
853
  }
520
854
  if (hasComposables) {
521
- sections.push("", "## Composables Used", "");
522
- for (const c of doc.composables) sections.push(`- \`${c.name}\``);
855
+ sections.push("", "## Composables Used");
856
+ for (const c of doc.composables) {
857
+ sections.push("", `### \`${c.name}\``);
858
+ if (c.source && (c.source.startsWith(".") || c.source.startsWith("@/"))) sections.push("", `*Source: \`${c.source}\`*`);
859
+ if (c.variables.length === 0) sections.push("", "Called for side effects.");
860
+ else if (!c.variables.some((v) => v.type) && c.variables.length <= 3) {
861
+ const vars = c.variables.map((v) => `\`${v.name}\``).join(", ");
862
+ sections.push("", `**Returns:** ${vars}`);
863
+ } else {
864
+ sections.push("");
865
+ sections.push("| Variable | Type |");
866
+ sections.push("| --- | --- |");
867
+ for (const v of c.variables) {
868
+ const type = v.type ? escHtml(esc(v.type)) : "-";
869
+ sections.push(`| ${esc(v.name)} | ${type} |`);
870
+ }
871
+ }
872
+ }
523
873
  }
524
874
  return sections.join("\n") + "\n";
525
875
  }
@@ -527,7 +877,7 @@ function generateMarkdown(doc) {
527
877
  //#region src/index.ts
528
878
  function parseComponent(filePath) {
529
879
  const abs = resolve(filePath);
530
- return parseSFC(readFileSync(abs, "utf-8"), abs.split("/").pop() ?? "Unknown.vue");
880
+ return parseSFC(readFileSync(abs, "utf-8"), abs.split("/").pop() ?? "Unknown.vue", abs.substring(0, abs.lastIndexOf("/")));
531
881
  }
532
882
  //#endregion
533
883
  //#region src/cli.ts
package/dist/index.d.mts CHANGED
@@ -25,8 +25,14 @@ interface ExposeDoc {
25
25
  type: string;
26
26
  description: string;
27
27
  }
28
+ interface ComposableVariable {
29
+ name: string;
30
+ type?: string;
31
+ }
28
32
  interface ComposableDoc {
29
33
  name: string;
34
+ source?: string;
35
+ variables: ComposableVariable[];
30
36
  }
31
37
  interface ComponentDoc {
32
38
  name: string;
@@ -40,7 +46,7 @@ interface ComponentDoc {
40
46
  }
41
47
  //#endregion
42
48
  //#region src/parser.d.ts
43
- declare function parseSFC(source: string, filename: string): ComponentDoc;
49
+ declare function parseSFC(source: string, filename: string, sfcDir?: string): ComponentDoc;
44
50
  //#endregion
45
51
  //#region src/markdown.d.ts
46
52
  declare function generateMarkdown(doc: ComponentDoc): string;
@@ -48,4 +54,4 @@ declare function generateMarkdown(doc: ComponentDoc): string;
48
54
  //#region src/index.d.ts
49
55
  declare function parseComponent(filePath: string): ComponentDoc;
50
56
  //#endregion
51
- export { type ComponentDoc, type ComposableDoc, type EmitDoc, type ExposeDoc, type PropDoc, type SlotDoc, generateMarkdown, parseComponent, parseSFC };
57
+ export { type ComponentDoc, type ComposableDoc, type ComposableVariable, type EmitDoc, type ExposeDoc, type PropDoc, type SlotDoc, generateMarkdown, parseComponent, parseSFC };
package/dist/index.mjs CHANGED
@@ -1,6 +1,269 @@
1
- import { readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { compileScript, parse } from "@vue/compiler-sfc";
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { babelParse, compileScript, parse } from "@vue/compiler-sfc";
4
+ //#region src/resolver.ts
5
+ function resolveImportPath(importSource, sfcDir) {
6
+ try {
7
+ if (!importSource.startsWith(".") && !importSource.startsWith("@/") && !importSource.startsWith("~/")) return null;
8
+ if (importSource.startsWith("./") || importSource.startsWith("../")) return tryResolveFile(resolve(sfcDir, importSource));
9
+ const tsconfig = findTsConfig(sfcDir);
10
+ if (!tsconfig) return null;
11
+ const { paths, baseUrl } = readTsConfigPaths(tsconfig);
12
+ if (!paths) return null;
13
+ const configDir = dirname(tsconfig);
14
+ const resolvedBaseUrl = baseUrl ? resolve(configDir, baseUrl) : configDir;
15
+ for (const [pattern, targets] of Object.entries(paths)) {
16
+ const prefix = pattern.replace(/\*$/, "");
17
+ if (!importSource.startsWith(prefix)) continue;
18
+ const remainder = importSource.slice(prefix.length);
19
+ for (const target of targets) {
20
+ const result = tryResolveFile(resolve(resolvedBaseUrl, target.replace(/\*$/, "") + remainder));
21
+ if (result) return result;
22
+ }
23
+ }
24
+ return null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ function tryResolveFile(basePath) {
30
+ if (existsSync(basePath) && !isDirectory(basePath)) return basePath;
31
+ for (const ext of [".ts", ".js"]) {
32
+ const candidate = basePath + ext;
33
+ if (existsSync(candidate)) return candidate;
34
+ }
35
+ for (const ext of ["/index.ts", "/index.js"]) {
36
+ const candidate = basePath + ext;
37
+ if (existsSync(candidate)) return candidate;
38
+ }
39
+ return null;
40
+ }
41
+ function isDirectory(filePath) {
42
+ try {
43
+ return statSync(filePath).isDirectory();
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+ function findTsConfig(startDir) {
49
+ let dir = resolve(startDir);
50
+ const root = resolve("/");
51
+ while (dir !== root) {
52
+ const tsconfig = join(dir, "tsconfig.json");
53
+ if (existsSync(tsconfig)) return tsconfig;
54
+ const jsconfig = join(dir, "jsconfig.json");
55
+ if (existsSync(jsconfig)) return jsconfig;
56
+ const parent = dirname(dir);
57
+ if (parent === dir) break;
58
+ dir = parent;
59
+ }
60
+ return null;
61
+ }
62
+ function readTsConfigPaths(configPath) {
63
+ try {
64
+ const content = JSON.parse(readFileSync(configPath, "utf-8"));
65
+ let paths = content.compilerOptions?.paths ?? null;
66
+ let baseUrl = content.compilerOptions?.baseUrl;
67
+ if (content.extends) {
68
+ const parentPath = resolve(dirname(configPath), content.extends);
69
+ const parentConfigFile = parentPath.endsWith(".json") ? parentPath : parentPath + ".json";
70
+ if (existsSync(parentConfigFile)) try {
71
+ const parentContent = JSON.parse(readFileSync(parentConfigFile, "utf-8"));
72
+ const parentPaths = parentContent.compilerOptions?.paths;
73
+ const parentBaseUrl = parentContent.compilerOptions?.baseUrl;
74
+ if (!paths && parentPaths) paths = parentPaths;
75
+ if (!baseUrl && parentBaseUrl) baseUrl = parentBaseUrl;
76
+ } catch {}
77
+ }
78
+ return {
79
+ paths,
80
+ baseUrl
81
+ };
82
+ } catch {
83
+ return {
84
+ paths: null,
85
+ baseUrl: void 0
86
+ };
87
+ }
88
+ }
89
+ function resolveComposableTypes(filePath, exportName, variableNames) {
90
+ try {
91
+ const funcNode = findExportedFunction(babelParse(readFileSync(filePath, "utf-8"), {
92
+ plugins: ["typescript", "jsx"],
93
+ sourceType: "module"
94
+ }).program.body, exportName);
95
+ if (!funcNode) return /* @__PURE__ */ new Map();
96
+ const body = getFunctionBody(funcNode);
97
+ if (!body) return /* @__PURE__ */ new Map();
98
+ const returnProps = findReturnProperties(body);
99
+ if (!returnProps) return /* @__PURE__ */ new Map();
100
+ const result = /* @__PURE__ */ new Map();
101
+ const nameSet = new Set(variableNames);
102
+ for (const prop of returnProps) {
103
+ let propName = null;
104
+ if (prop.type === "ObjectProperty") propName = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : null;
105
+ else if (prop.type === "ObjectMethod") {
106
+ propName = prop.key.type === "Identifier" ? prop.key.name : null;
107
+ if (propName && nameSet.has(propName)) result.set(propName, inferFunctionSignature(prop));
108
+ continue;
109
+ } else if (prop.type === "SpreadElement") continue;
110
+ if (!propName || !nameSet.has(propName)) continue;
111
+ if (prop.type === "ObjectProperty" && prop.shorthand) {
112
+ const type = traceVariableType(propName, body);
113
+ result.set(propName, type);
114
+ } else if (prop.type === "ObjectProperty") {
115
+ const type = inferType(prop.value);
116
+ result.set(propName, type);
117
+ }
118
+ }
119
+ return result;
120
+ } catch {
121
+ return /* @__PURE__ */ new Map();
122
+ }
123
+ }
124
+ function findExportedFunction(stmts, exportName) {
125
+ for (const stmt of stmts) {
126
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "FunctionDeclaration" && stmt.declaration.id?.name === exportName) return stmt.declaration;
127
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "VariableDeclaration") {
128
+ for (const decl of stmt.declaration.declarations) if (decl.id.type === "Identifier" && decl.id.name === exportName && decl.init && (decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")) return decl.init;
129
+ }
130
+ if (stmt.type === "ExportDefaultDeclaration" && stmt.declaration.type === "FunctionDeclaration" && stmt.declaration.id?.name === exportName) return stmt.declaration;
131
+ if (stmt.type === "ExportDefaultDeclaration" && stmt.declaration.type === "FunctionDeclaration" && !stmt.declaration.id) return stmt.declaration;
132
+ if (stmt.type === "ExportDefaultDeclaration" && (stmt.declaration.type === "ArrowFunctionExpression" || stmt.declaration.type === "FunctionExpression")) return stmt.declaration;
133
+ if (stmt.type === "FunctionDeclaration" && stmt.id?.name === exportName) {
134
+ if (stmts.some((s) => s.type === "ExportNamedDeclaration" && !s.declaration && s.specifiers.some((spec) => spec.type === "ExportSpecifier" && (spec.local.type === "Identifier" && spec.local.name === exportName || spec.exported.type === "Identifier" && spec.exported.name === exportName)))) return stmt;
135
+ }
136
+ if (stmt.type === "VariableDeclaration") {
137
+ for (const decl of stmt.declarations) if (decl.id.type === "Identifier" && decl.id.name === exportName && decl.init && (decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")) {
138
+ if (stmts.some((s) => s.type === "ExportNamedDeclaration" && !s.declaration && s.specifiers.some((spec) => spec.type === "ExportSpecifier" && (spec.local.type === "Identifier" && spec.local.name === exportName || spec.exported.type === "Identifier" && spec.exported.name === exportName)))) return decl.init;
139
+ }
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+ function getFunctionBody(node) {
145
+ if (node.body.type === "BlockStatement") return node.body.body;
146
+ return null;
147
+ }
148
+ function findReturnProperties(body) {
149
+ for (let i = body.length - 1; i >= 0; i--) {
150
+ const stmt = body[i];
151
+ if (stmt.type === "ReturnStatement" && stmt.argument?.type === "ObjectExpression") return stmt.argument.properties;
152
+ }
153
+ return null;
154
+ }
155
+ function traceVariableType(name, body) {
156
+ for (let i = body.length - 1; i >= 0; i--) {
157
+ const stmt = body[i];
158
+ if (stmt.type === "FunctionDeclaration" && stmt.id?.name === name) return inferFunctionSignature(stmt);
159
+ if (stmt.type === "VariableDeclaration") {
160
+ for (const decl of stmt.declarations) if (decl.id.type === "Identifier" && decl.id.name === name && decl.init) return inferType(decl.init);
161
+ }
162
+ }
163
+ return "unknown";
164
+ }
165
+ function inferType(node) {
166
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "ref") {
167
+ const typeParams = node.typeParameters;
168
+ if (typeParams?.params?.length > 0) return `Ref<${resolveTypeAnnotation(typeParams.params[0])}>`;
169
+ const arg = node.arguments[0];
170
+ if (!arg) return "Ref<unknown>";
171
+ return `Ref<${inferLiteralType(arg)}>`;
172
+ }
173
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "computed") return "ComputedRef";
174
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "reactive") return "Object";
175
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") return inferFunctionSignature(node);
176
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && /^use[A-Z]/.test(node.callee.name)) return "unknown";
177
+ return inferLiteralType(node);
178
+ }
179
+ function inferLiteralType(node) {
180
+ switch (node.type) {
181
+ case "NumericLiteral": return "number";
182
+ case "StringLiteral": return "string";
183
+ case "BooleanLiteral": return "boolean";
184
+ case "NullLiteral": return "null";
185
+ case "TemplateLiteral": return "string";
186
+ case "ArrayExpression": return "Array";
187
+ case "ObjectExpression": return "Object";
188
+ default: return "unknown";
189
+ }
190
+ }
191
+ function inferFunctionSignature(node) {
192
+ return `(${extractParams(node.params ?? [])}) => ${extractReturnType(node)}`;
193
+ }
194
+ function extractParams(params) {
195
+ return params.map((param) => {
196
+ if (param.type === "Identifier") {
197
+ const annotation = param.typeAnnotation?.typeAnnotation;
198
+ if (annotation) return `${param.name}: ${resolveTypeAnnotation(annotation)}`;
199
+ return param.name;
200
+ }
201
+ if (param.type === "AssignmentPattern") {
202
+ const left = param.left;
203
+ if (left.type === "Identifier") {
204
+ const annotation = left.typeAnnotation?.typeAnnotation;
205
+ if (annotation) return `${left.name}: ${resolveTypeAnnotation(annotation)}`;
206
+ return left.name;
207
+ }
208
+ return "arg";
209
+ }
210
+ if (param.type === "RestElement") {
211
+ const arg = param.argument;
212
+ if (arg.type === "Identifier") {
213
+ const annotation = arg.typeAnnotation?.typeAnnotation;
214
+ if (annotation) return `...${arg.name}: ${resolveTypeAnnotation(annotation)}`;
215
+ return `...${arg.name}`;
216
+ }
217
+ return "...args";
218
+ }
219
+ if (param.type === "ObjectPattern") return "options";
220
+ if (param.type === "ArrayPattern") return "args";
221
+ return "arg";
222
+ }).join(", ");
223
+ }
224
+ function extractReturnType(node) {
225
+ const annotation = node.returnType?.typeAnnotation ?? node.typeAnnotation?.typeAnnotation;
226
+ let baseType;
227
+ if (annotation) baseType = resolveTypeAnnotation(annotation);
228
+ else baseType = "void";
229
+ if (node.async && baseType !== "void") return `Promise<${baseType}>`;
230
+ if (node.async) return "Promise<void>";
231
+ return baseType;
232
+ }
233
+ function resolveTypeAnnotation(node) {
234
+ if (!node) return "unknown";
235
+ switch (node.type) {
236
+ case "TSStringKeyword": return "string";
237
+ case "TSNumberKeyword": return "number";
238
+ case "TSBooleanKeyword": return "boolean";
239
+ case "TSVoidKeyword": return "void";
240
+ case "TSAnyKeyword": return "any";
241
+ case "TSNullKeyword": return "null";
242
+ case "TSUndefinedKeyword": return "undefined";
243
+ case "TSObjectKeyword": return "object";
244
+ case "TSNeverKeyword": return "never";
245
+ case "TSUnknownKeyword": return "unknown";
246
+ case "TSTypeReference": {
247
+ const name = node.typeName?.type === "Identifier" ? node.typeName.name : node.typeName?.type === "TSQualifiedName" ? `${node.typeName.left?.name ?? ""}.${node.typeName.right?.name ?? ""}` : "unknown";
248
+ if (node.typeParameters?.params?.length > 0) return `${name}<${node.typeParameters.params.map((p) => resolveTypeAnnotation(p)).join(", ")}>`;
249
+ return name;
250
+ }
251
+ case "TSUnionType": return node.types.map((t) => resolveTypeAnnotation(t)).join(" | ");
252
+ case "TSIntersectionType": return node.types.map((t) => resolveTypeAnnotation(t)).join(" & ");
253
+ case "TSArrayType": return `${resolveTypeAnnotation(node.elementType)}[]`;
254
+ case "TSLiteralType":
255
+ if (node.literal.type === "StringLiteral") return `'${node.literal.value}'`;
256
+ if (node.literal.type === "NumericLiteral") return String(node.literal.value);
257
+ if (node.literal.type === "BooleanLiteral") return String(node.literal.value);
258
+ return "unknown";
259
+ case "TSFunctionType": return "Function";
260
+ case "TSTupleType": return `[${(node.elementTypes ?? []).map((t) => resolveTypeAnnotation(t)).join(", ")}]`;
261
+ case "TSParenthesizedType": return resolveTypeAnnotation(node.typeAnnotation);
262
+ case "TSTypeLiteral": return "object";
263
+ default: return "unknown";
264
+ }
265
+ }
266
+ //#endregion
4
267
  //#region src/parser.ts
5
268
  function parseJSDocTags(comments) {
6
269
  const result = { description: "" };
@@ -21,7 +284,7 @@ function parseJSDocTags(comments) {
21
284
  result.description = descLines.join(" ");
22
285
  return result;
23
286
  }
24
- function parseSFC(source, filename) {
287
+ function parseSFC(source, filename, sfcDir) {
25
288
  const doc = {
26
289
  name: filename.replace(/\.vue$/, "").split("/").pop() ?? "Unknown",
27
290
  props: [],
@@ -49,7 +312,7 @@ function parseSFC(source, filename) {
49
312
  else if (callee === "defineSlots" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.slots = extractTypeSlots(typeParams.params[0]);
50
313
  else if (callee === "defineExpose" && args[0]?.type === "ObjectExpression") doc.exposes = extractExposes(args[0], scriptSource);
51
314
  }
52
- doc.composables = extractComposables(setupAst);
315
+ doc.composables = extractComposables(setupAst, buildImportMap(setupAst), sfcDir);
53
316
  }
54
317
  const scriptAst = compiled.scriptAst;
55
318
  if (scriptAst && doc.props.length === 0 && doc.emits.length === 0) {
@@ -330,26 +593,94 @@ function extractExposes(obj, _source) {
330
593
  }
331
594
  return exposes;
332
595
  }
333
- function extractComposables(ast) {
596
+ function buildImportMap(ast) {
597
+ const map = /* @__PURE__ */ new Map();
598
+ for (const stmt of ast) if (stmt.type === "ImportDeclaration") {
599
+ for (const spec of stmt.specifiers ?? []) if (spec.type === "ImportSpecifier" || spec.type === "ImportDefaultSpecifier") map.set(spec.local.name, stmt.source.value);
600
+ }
601
+ return map;
602
+ }
603
+ function extractVariablesFromPattern(decl) {
604
+ const id = decl.id;
605
+ if (!id) return [];
606
+ if (id.type === "Identifier") {
607
+ const v = { name: id.name };
608
+ if (id.typeAnnotation?.typeAnnotation) v.type = resolveTypeString(id.typeAnnotation.typeAnnotation);
609
+ return [v];
610
+ }
611
+ if (id.type === "ObjectPattern") {
612
+ const vars = [];
613
+ const typeAnnotation = id.typeAnnotation?.typeAnnotation;
614
+ const typeMembers = typeAnnotation?.type === "TSTypeLiteral" ? typeAnnotation.members : null;
615
+ for (const prop of id.properties) if (prop.type === "RestElement") {
616
+ const name = prop.argument?.name ?? "rest";
617
+ vars.push({ name });
618
+ } else if (prop.type === "ObjectProperty") {
619
+ const name = prop.value?.type === "Identifier" ? prop.value.name : prop.value?.type === "AssignmentPattern" && prop.value.left?.type === "Identifier" ? prop.value.left.name : prop.key?.type === "Identifier" ? prop.key.name : "";
620
+ if (!name) continue;
621
+ const v = { name };
622
+ if (typeMembers) {
623
+ const keyName = prop.key?.type === "Identifier" ? prop.key.name : "";
624
+ for (const member of typeMembers) if (member.type === "TSPropertySignature" && member.key?.type === "Identifier" && member.key.name === keyName && member.typeAnnotation?.typeAnnotation) {
625
+ v.type = resolveTypeString(member.typeAnnotation.typeAnnotation);
626
+ break;
627
+ }
628
+ }
629
+ vars.push(v);
630
+ }
631
+ return vars;
632
+ }
633
+ if (id.type === "ArrayPattern") {
634
+ const vars = [];
635
+ for (const el of id.elements) {
636
+ if (!el) continue;
637
+ if (el.type === "Identifier") vars.push({ name: el.name });
638
+ else if (el.type === "RestElement" && el.argument?.type === "Identifier") vars.push({ name: el.argument.name });
639
+ else if (el.type === "AssignmentPattern" && el.left?.type === "Identifier") vars.push({ name: el.left.name });
640
+ }
641
+ return vars;
642
+ }
643
+ return [];
644
+ }
645
+ function extractComposables(ast, importMap, sfcDir) {
334
646
  const seen = /* @__PURE__ */ new Set();
335
647
  const composables = [];
336
648
  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 });
649
+ if (stmt.type === "ExpressionStatement" && stmt.expression.type === "CallExpression" && stmt.expression.callee.type === "Identifier" && /^use[A-Z]/.test(stmt.expression.callee.name)) {
650
+ const name = stmt.expression.callee.name;
651
+ if (!seen.has(name)) {
652
+ seen.add(name);
653
+ composables.push({
654
+ name,
655
+ source: importMap.get(name),
656
+ variables: []
657
+ });
658
+ }
659
+ }
660
+ if (stmt.type === "VariableDeclaration") {
661
+ for (const decl of stmt.declarations) if (decl.init?.type === "CallExpression" && decl.init.callee.type === "Identifier" && /^use[A-Z]/.test(decl.init.callee.name)) {
662
+ const name = decl.init.callee.name;
663
+ if (seen.has(name)) continue;
664
+ seen.add(name);
665
+ const variables = extractVariablesFromPattern(decl);
666
+ const source = importMap.get(name);
667
+ if (variables.some((v) => !v.type) && sfcDir && source) {
668
+ const resolvedPath = resolveImportPath(source, sfcDir);
669
+ if (resolvedPath) {
670
+ const typeMap = resolveComposableTypes(resolvedPath, name, variables.filter((v) => !v.type).map((v) => v.name));
671
+ for (const v of variables) if (!v.type && typeMap.has(v.name)) v.type = typeMap.get(v.name);
672
+ }
673
+ }
674
+ composables.push({
675
+ name,
676
+ source,
677
+ variables
678
+ });
679
+ }
341
680
  }
342
681
  }
343
682
  return composables;
344
683
  }
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
684
  function extractTemplateSlots(templateAst) {
354
685
  const slots = [];
355
686
  walkTemplate(templateAst.children ?? [], slots);
@@ -447,6 +778,9 @@ function stringifyDefault(node, source) {
447
778
  function esc(value) {
448
779
  return value.replaceAll("|", "\\|");
449
780
  }
781
+ function escHtml(value) {
782
+ return value.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
783
+ }
450
784
  function generateMarkdown(doc) {
451
785
  const sections = [`# ${doc.name}`];
452
786
  if (doc.description) sections.push("", doc.description);
@@ -517,8 +851,24 @@ function generateMarkdown(doc) {
517
851
  }
518
852
  }
519
853
  if (hasComposables) {
520
- sections.push("", "## Composables Used", "");
521
- for (const c of doc.composables) sections.push(`- \`${c.name}\``);
854
+ sections.push("", "## Composables Used");
855
+ for (const c of doc.composables) {
856
+ sections.push("", `### \`${c.name}\``);
857
+ if (c.source && (c.source.startsWith(".") || c.source.startsWith("@/"))) sections.push("", `*Source: \`${c.source}\`*`);
858
+ if (c.variables.length === 0) sections.push("", "Called for side effects.");
859
+ else if (!c.variables.some((v) => v.type) && c.variables.length <= 3) {
860
+ const vars = c.variables.map((v) => `\`${v.name}\``).join(", ");
861
+ sections.push("", `**Returns:** ${vars}`);
862
+ } else {
863
+ sections.push("");
864
+ sections.push("| Variable | Type |");
865
+ sections.push("| --- | --- |");
866
+ for (const v of c.variables) {
867
+ const type = v.type ? escHtml(esc(v.type)) : "-";
868
+ sections.push(`| ${esc(v.name)} | ${type} |`);
869
+ }
870
+ }
871
+ }
522
872
  }
523
873
  return sections.join("\n") + "\n";
524
874
  }
@@ -526,7 +876,7 @@ function generateMarkdown(doc) {
526
876
  //#region src/index.ts
527
877
  function parseComponent(filePath) {
528
878
  const abs = resolve(filePath);
529
- return parseSFC(readFileSync(abs, "utf-8"), abs.split("/").pop() ?? "Unknown.vue");
879
+ return parseSFC(readFileSync(abs, "utf-8"), abs.split("/").pop() ?? "Unknown.vue", abs.substring(0, abs.lastIndexOf("/")));
530
880
  }
531
881
  //#endregion
532
882
  export { generateMarkdown, parseComponent, parseSFC };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compmark-vue",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Auto-generate Markdown documentation from Vue 3 SFCs",
5
5
  "license": "MIT",
6
6
  "repository": "noopurphalak/compmark-vue",