compmark-vue 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,6 +1,320 @@
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
+ import { glob } from "tinyglobby";
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$1(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$1(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
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
4
318
  //#region src/parser.ts
5
319
  function parseJSDocTags(comments) {
6
320
  const result = { description: "" };
@@ -21,35 +335,61 @@ function parseJSDocTags(comments) {
21
335
  result.description = descLines.join(" ");
22
336
  return result;
23
337
  }
24
- function parseSFC(source, filename) {
338
+ function parseSFC(source, filename, sfcDir) {
25
339
  const doc = {
26
340
  name: filename.replace(/\.vue$/, "").split("/").pop() ?? "Unknown",
27
341
  props: [],
28
342
  emits: []
29
343
  };
30
- const { descriptor } = parse(source, { filename });
344
+ const fullPath = sfcDir ? `${sfcDir}/${filename}` : filename;
345
+ const { descriptor } = parse(source, { filename: fullPath });
346
+ doc.scriptSetup = !!descriptor.scriptSetup;
31
347
  if (descriptor.template?.ast) {
32
348
  const templateSlots = extractTemplateSlots(descriptor.template.ast);
33
349
  if (templateSlots.length > 0) doc.slots = templateSlots;
34
350
  }
35
351
  if (!descriptor.scriptSetup && !descriptor.script) return doc;
36
- const compiled = compileScript(descriptor, { id: filename });
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
+ }
37
370
  const componentJSDoc = extractComponentJSDoc(compiled.scriptSetupAst ?? compiled.scriptAst ?? []);
38
371
  doc.description = componentJSDoc.description;
39
372
  doc.internal = componentJSDoc.internal;
40
373
  const setupAst = compiled.scriptSetupAst;
41
374
  if (setupAst) {
42
375
  const scriptSource = descriptor.scriptSetup?.content ?? compiled.content;
376
+ const importMap = buildImportMap(setupAst);
43
377
  for (const stmt of setupAst) {
44
378
  const calls = extractDefineCalls(stmt);
45
379
  for (const { callee, args, leadingComments, typeParams, defaultsArg } of calls) if (callee === "defineProps" && args[0]?.type === "ObjectExpression") doc.props = extractProps(args[0], scriptSource);
46
380
  else if (callee === "defineProps" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.props = extractTypeProps(typeParams.params[0], defaultsArg, scriptSource);
47
- else if (callee === "defineEmits" && args[0]?.type === "ArrayExpression") doc.emits = extractEmits(args[0], leadingComments);
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);
48
388
  else if (callee === "defineEmits" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.emits = extractTypeEmits(typeParams.params[0]);
49
389
  else if (callee === "defineSlots" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.slots = extractTypeSlots(typeParams.params[0]);
50
390
  else if (callee === "defineExpose" && args[0]?.type === "ObjectExpression") doc.exposes = extractExposes(args[0], scriptSource);
51
391
  }
52
- doc.composables = extractComposables(setupAst);
392
+ doc.composables = extractComposables(setupAst, importMap, sfcDir);
53
393
  }
54
394
  const scriptAst = compiled.scriptAst;
55
395
  if (scriptAst && doc.props.length === 0 && doc.emits.length === 0) {
@@ -330,26 +670,94 @@ function extractExposes(obj, _source) {
330
670
  }
331
671
  return exposes;
332
672
  }
333
- function extractComposables(ast) {
673
+ function buildImportMap(ast) {
674
+ const map = /* @__PURE__ */ new Map();
675
+ for (const stmt of ast) if (stmt.type === "ImportDeclaration") {
676
+ for (const spec of stmt.specifiers ?? []) if (spec.type === "ImportSpecifier" || spec.type === "ImportDefaultSpecifier") map.set(spec.local.name, stmt.source.value);
677
+ }
678
+ return map;
679
+ }
680
+ function extractVariablesFromPattern(decl) {
681
+ const id = decl.id;
682
+ if (!id) return [];
683
+ if (id.type === "Identifier") {
684
+ const v = { name: id.name };
685
+ if (id.typeAnnotation?.typeAnnotation) v.type = resolveTypeString(id.typeAnnotation.typeAnnotation);
686
+ return [v];
687
+ }
688
+ if (id.type === "ObjectPattern") {
689
+ const vars = [];
690
+ const typeAnnotation = id.typeAnnotation?.typeAnnotation;
691
+ const typeMembers = typeAnnotation?.type === "TSTypeLiteral" ? typeAnnotation.members : null;
692
+ for (const prop of id.properties) if (prop.type === "RestElement") {
693
+ const name = prop.argument?.name ?? "rest";
694
+ vars.push({ name });
695
+ } else if (prop.type === "ObjectProperty") {
696
+ 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 : "";
697
+ if (!name) continue;
698
+ const v = { name };
699
+ if (typeMembers) {
700
+ const keyName = prop.key?.type === "Identifier" ? prop.key.name : "";
701
+ for (const member of typeMembers) if (member.type === "TSPropertySignature" && member.key?.type === "Identifier" && member.key.name === keyName && member.typeAnnotation?.typeAnnotation) {
702
+ v.type = resolveTypeString(member.typeAnnotation.typeAnnotation);
703
+ break;
704
+ }
705
+ }
706
+ vars.push(v);
707
+ }
708
+ return vars;
709
+ }
710
+ if (id.type === "ArrayPattern") {
711
+ const vars = [];
712
+ for (const el of id.elements) {
713
+ if (!el) continue;
714
+ if (el.type === "Identifier") vars.push({ name: el.name });
715
+ else if (el.type === "RestElement" && el.argument?.type === "Identifier") vars.push({ name: el.argument.name });
716
+ else if (el.type === "AssignmentPattern" && el.left?.type === "Identifier") vars.push({ name: el.left.name });
717
+ }
718
+ return vars;
719
+ }
720
+ return [];
721
+ }
722
+ function extractComposables(ast, importMap, sfcDir) {
334
723
  const seen = /* @__PURE__ */ new Set();
335
724
  const composables = [];
336
725
  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 });
726
+ if (stmt.type === "ExpressionStatement" && stmt.expression.type === "CallExpression" && stmt.expression.callee.type === "Identifier" && /^use[A-Z]/.test(stmt.expression.callee.name)) {
727
+ const name = stmt.expression.callee.name;
728
+ if (!seen.has(name)) {
729
+ seen.add(name);
730
+ composables.push({
731
+ name,
732
+ source: importMap.get(name),
733
+ variables: []
734
+ });
735
+ }
736
+ }
737
+ if (stmt.type === "VariableDeclaration") {
738
+ for (const decl of stmt.declarations) if (decl.init?.type === "CallExpression" && decl.init.callee.type === "Identifier" && /^use[A-Z]/.test(decl.init.callee.name)) {
739
+ const name = decl.init.callee.name;
740
+ if (seen.has(name)) continue;
741
+ seen.add(name);
742
+ const variables = extractVariablesFromPattern(decl);
743
+ const source = importMap.get(name);
744
+ if (variables.some((v) => !v.type) && sfcDir && source) {
745
+ const resolvedPath = resolveImportPath(source, sfcDir);
746
+ if (resolvedPath) {
747
+ const typeMap = resolveComposableTypes(resolvedPath, name, variables.filter((v) => !v.type).map((v) => v.name));
748
+ for (const v of variables) if (!v.type && typeMap.has(v.name)) v.type = typeMap.get(v.name);
749
+ }
750
+ }
751
+ composables.push({
752
+ name,
753
+ source,
754
+ variables
755
+ });
756
+ }
341
757
  }
342
758
  }
343
759
  return composables;
344
760
  }
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
761
  function extractTemplateSlots(templateAst) {
354
762
  const slots = [];
355
763
  walkTemplate(templateAst.children ?? [], slots);
@@ -447,9 +855,13 @@ function stringifyDefault(node, source) {
447
855
  function esc(value) {
448
856
  return value.replaceAll("|", "\\|");
449
857
  }
858
+ function escHtml(value) {
859
+ return value.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
860
+ }
450
861
  function generateMarkdown(doc) {
451
862
  const sections = [`# ${doc.name}`];
452
863
  if (doc.description) sections.push("", doc.description);
864
+ if (doc.scriptSetup) sections.push("", "**Note:** Uses `<script setup>` syntax.");
453
865
  const hasProps = doc.props.length > 0;
454
866
  const hasEmits = doc.emits.length > 0;
455
867
  const hasSlots = (doc.slots?.length ?? 0) > 0;
@@ -517,16 +929,113 @@ function generateMarkdown(doc) {
517
929
  }
518
930
  }
519
931
  if (hasComposables) {
520
- sections.push("", "## Composables Used", "");
521
- for (const c of doc.composables) sections.push(`- \`${c.name}\``);
932
+ sections.push("", "## Composables Used");
933
+ for (const c of doc.composables) {
934
+ sections.push("", `### \`${c.name}\``);
935
+ if (c.source && (c.source.startsWith(".") || c.source.startsWith("@/"))) sections.push("", `*Source: \`${c.source}\`*`);
936
+ if (c.variables.length === 0) sections.push("", "Called for side effects.");
937
+ else if (!c.variables.some((v) => v.type) && c.variables.length <= 3) {
938
+ const vars = c.variables.map((v) => `\`${v.name}\``).join(", ");
939
+ sections.push("", `**Returns:** ${vars}`);
940
+ } else {
941
+ sections.push("");
942
+ sections.push("| Variable | Type |");
943
+ sections.push("| --- | --- |");
944
+ for (const v of c.variables) {
945
+ const type = v.type ? escHtml(esc(v.type)) : "-";
946
+ sections.push(`| ${esc(v.name)} | ${type} |`);
947
+ }
948
+ }
949
+ }
522
950
  }
523
951
  return sections.join("\n") + "\n";
524
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
+ }
525
1034
  //#endregion
526
1035
  //#region src/index.ts
527
1036
  function parseComponent(filePath) {
528
1037
  const abs = resolve(filePath);
529
- return parseSFC(readFileSync(abs, "utf-8"), abs.split("/").pop() ?? "Unknown.vue");
1038
+ return parseSFC(readFileSync(abs, "utf-8"), abs.split("/").pop() ?? "Unknown.vue", abs.substring(0, abs.lastIndexOf("/")));
530
1039
  }
531
1040
  //#endregion
532
- 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.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Auto-generate Markdown documentation from Vue 3 SFCs",
5
5
  "license": "MIT",
6
6
  "repository": "noopurphalak/compmark-vue",
7
7
  "bin": {
8
- "compmark": "./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
  }