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/cli.mjs CHANGED
@@ -1,7 +1,360 @@
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 { defineCommand, runMain } from "citty";
3
+ import { existsSync, mkdirSync, readFileSync, statSync, watch, writeFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { glob } from "tinyglobby";
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
45
+ //#region src/resolver.ts
46
+ function resolveImportPath(importSource, sfcDir) {
47
+ try {
48
+ if (!importSource.startsWith(".") && !importSource.startsWith("@/") && !importSource.startsWith("~/")) return null;
49
+ if (importSource.startsWith("./") || importSource.startsWith("../")) return tryResolveFile(resolve(sfcDir, importSource));
50
+ const tsconfig = findTsConfig(sfcDir);
51
+ if (!tsconfig) return null;
52
+ const { paths, baseUrl } = readTsConfigPaths(tsconfig);
53
+ if (!paths) return null;
54
+ const configDir = dirname(tsconfig);
55
+ const resolvedBaseUrl = baseUrl ? resolve(configDir, baseUrl) : configDir;
56
+ for (const [pattern, targets] of Object.entries(paths)) {
57
+ const prefix = pattern.replace(/\*$/, "");
58
+ if (!importSource.startsWith(prefix)) continue;
59
+ const remainder = importSource.slice(prefix.length);
60
+ for (const target of targets) {
61
+ const result = tryResolveFile(resolve(resolvedBaseUrl, target.replace(/\*$/, "") + remainder));
62
+ if (result) return result;
63
+ }
64
+ }
65
+ return null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ function tryResolveFile(basePath) {
71
+ if (existsSync(basePath) && !isDirectory(basePath)) return basePath;
72
+ for (const ext of [".ts", ".js"]) {
73
+ const candidate = basePath + ext;
74
+ if (existsSync(candidate)) return candidate;
75
+ }
76
+ for (const ext of ["/index.ts", "/index.js"]) {
77
+ const candidate = basePath + ext;
78
+ if (existsSync(candidate)) return candidate;
79
+ }
80
+ return null;
81
+ }
82
+ function isDirectory(filePath) {
83
+ try {
84
+ return statSync(filePath).isDirectory();
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+ function findTsConfig(startDir) {
90
+ let dir = resolve(startDir);
91
+ const root = resolve("/");
92
+ while (dir !== root) {
93
+ const tsconfig = join(dir, "tsconfig.json");
94
+ if (existsSync(tsconfig)) return tsconfig;
95
+ const jsconfig = join(dir, "jsconfig.json");
96
+ if (existsSync(jsconfig)) return jsconfig;
97
+ const parent = dirname(dir);
98
+ if (parent === dir) break;
99
+ dir = parent;
100
+ }
101
+ return null;
102
+ }
103
+ function readTsConfigPaths(configPath) {
104
+ try {
105
+ const content = JSON.parse(readFileSync(configPath, "utf-8"));
106
+ let paths = content.compilerOptions?.paths ?? null;
107
+ let baseUrl = content.compilerOptions?.baseUrl;
108
+ if (content.extends) {
109
+ const parentPath = resolve(dirname(configPath), content.extends);
110
+ const parentConfigFile = parentPath.endsWith(".json") ? parentPath : parentPath + ".json";
111
+ if (existsSync(parentConfigFile)) try {
112
+ const parentContent = JSON.parse(readFileSync(parentConfigFile, "utf-8"));
113
+ const parentPaths = parentContent.compilerOptions?.paths;
114
+ const parentBaseUrl = parentContent.compilerOptions?.baseUrl;
115
+ if (!paths && parentPaths) paths = parentPaths;
116
+ if (!baseUrl && parentBaseUrl) baseUrl = parentBaseUrl;
117
+ } catch {}
118
+ }
119
+ return {
120
+ paths,
121
+ baseUrl
122
+ };
123
+ } catch {
124
+ return {
125
+ paths: null,
126
+ baseUrl: void 0
127
+ };
128
+ }
129
+ }
130
+ function resolveComposableTypes(filePath, exportName, variableNames) {
131
+ try {
132
+ const funcNode = findExportedFunction(babelParse(readFileSync(filePath, "utf-8"), {
133
+ plugins: ["typescript", "jsx"],
134
+ sourceType: "module"
135
+ }).program.body, exportName);
136
+ if (!funcNode) return /* @__PURE__ */ new Map();
137
+ const body = getFunctionBody(funcNode);
138
+ if (!body) return /* @__PURE__ */ new Map();
139
+ const returnProps = findReturnProperties(body);
140
+ if (!returnProps) return /* @__PURE__ */ new Map();
141
+ const result = /* @__PURE__ */ new Map();
142
+ const nameSet = new Set(variableNames);
143
+ for (const prop of returnProps) {
144
+ let propName = null;
145
+ if (prop.type === "ObjectProperty") propName = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : null;
146
+ else if (prop.type === "ObjectMethod") {
147
+ propName = prop.key.type === "Identifier" ? prop.key.name : null;
148
+ if (propName && nameSet.has(propName)) result.set(propName, inferFunctionSignature(prop));
149
+ continue;
150
+ } else if (prop.type === "SpreadElement") continue;
151
+ if (!propName || !nameSet.has(propName)) continue;
152
+ if (prop.type === "ObjectProperty" && prop.shorthand) {
153
+ const type = traceVariableType(propName, body);
154
+ result.set(propName, type);
155
+ } else if (prop.type === "ObjectProperty") {
156
+ const type = inferType(prop.value);
157
+ result.set(propName, type);
158
+ }
159
+ }
160
+ return result;
161
+ } catch {
162
+ return /* @__PURE__ */ new Map();
163
+ }
164
+ }
165
+ function findExportedFunction(stmts, exportName) {
166
+ for (const stmt of stmts) {
167
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "FunctionDeclaration" && stmt.declaration.id?.name === exportName) return stmt.declaration;
168
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "VariableDeclaration") {
169
+ 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;
170
+ }
171
+ if (stmt.type === "ExportDefaultDeclaration" && stmt.declaration.type === "FunctionDeclaration" && stmt.declaration.id?.name === exportName) return stmt.declaration;
172
+ if (stmt.type === "ExportDefaultDeclaration" && stmt.declaration.type === "FunctionDeclaration" && !stmt.declaration.id) return stmt.declaration;
173
+ if (stmt.type === "ExportDefaultDeclaration" && (stmt.declaration.type === "ArrowFunctionExpression" || stmt.declaration.type === "FunctionExpression")) return stmt.declaration;
174
+ if (stmt.type === "FunctionDeclaration" && stmt.id?.name === exportName) {
175
+ 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;
176
+ }
177
+ if (stmt.type === "VariableDeclaration") {
178
+ 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")) {
179
+ 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;
180
+ }
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+ function getFunctionBody(node) {
186
+ if (node.body.type === "BlockStatement") return node.body.body;
187
+ return null;
188
+ }
189
+ function findReturnProperties(body) {
190
+ for (let i = body.length - 1; i >= 0; i--) {
191
+ const stmt = body[i];
192
+ if (stmt.type === "ReturnStatement" && stmt.argument?.type === "ObjectExpression") return stmt.argument.properties;
193
+ }
194
+ return null;
195
+ }
196
+ function traceVariableType(name, body) {
197
+ for (let i = body.length - 1; i >= 0; i--) {
198
+ const stmt = body[i];
199
+ if (stmt.type === "FunctionDeclaration" && stmt.id?.name === name) return inferFunctionSignature(stmt);
200
+ if (stmt.type === "VariableDeclaration") {
201
+ for (const decl of stmt.declarations) if (decl.id.type === "Identifier" && decl.id.name === name && decl.init) return inferType(decl.init);
202
+ }
203
+ }
204
+ return "unknown";
205
+ }
206
+ function inferType(node) {
207
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "ref") {
208
+ const typeParams = node.typeParameters;
209
+ if (typeParams?.params?.length > 0) return `Ref<${resolveTypeAnnotation(typeParams.params[0])}>`;
210
+ const arg = node.arguments[0];
211
+ if (!arg) return "Ref<unknown>";
212
+ return `Ref<${inferLiteralType(arg)}>`;
213
+ }
214
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "computed") return "ComputedRef";
215
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "reactive") return "Object";
216
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") return inferFunctionSignature(node);
217
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && /^use[A-Z]/.test(node.callee.name)) return "unknown";
218
+ return inferLiteralType(node);
219
+ }
220
+ function inferLiteralType(node) {
221
+ switch (node.type) {
222
+ case "NumericLiteral": return "number";
223
+ case "StringLiteral": return "string";
224
+ case "BooleanLiteral": return "boolean";
225
+ case "NullLiteral": return "null";
226
+ case "TemplateLiteral": return "string";
227
+ case "ArrayExpression": return "Array";
228
+ case "ObjectExpression": return "Object";
229
+ default: return "unknown";
230
+ }
231
+ }
232
+ function inferFunctionSignature(node) {
233
+ return `(${extractParams(node.params ?? [])}) => ${extractReturnType(node)}`;
234
+ }
235
+ function extractParams(params) {
236
+ return params.map((param) => {
237
+ if (param.type === "Identifier") {
238
+ const annotation = param.typeAnnotation?.typeAnnotation;
239
+ if (annotation) return `${param.name}: ${resolveTypeAnnotation(annotation)}`;
240
+ return param.name;
241
+ }
242
+ if (param.type === "AssignmentPattern") {
243
+ const left = param.left;
244
+ if (left.type === "Identifier") {
245
+ const annotation = left.typeAnnotation?.typeAnnotation;
246
+ if (annotation) return `${left.name}: ${resolveTypeAnnotation(annotation)}`;
247
+ return left.name;
248
+ }
249
+ return "arg";
250
+ }
251
+ if (param.type === "RestElement") {
252
+ const arg = param.argument;
253
+ if (arg.type === "Identifier") {
254
+ const annotation = arg.typeAnnotation?.typeAnnotation;
255
+ if (annotation) return `...${arg.name}: ${resolveTypeAnnotation(annotation)}`;
256
+ return `...${arg.name}`;
257
+ }
258
+ return "...args";
259
+ }
260
+ if (param.type === "ObjectPattern") return "options";
261
+ if (param.type === "ArrayPattern") return "args";
262
+ return "arg";
263
+ }).join(", ");
264
+ }
265
+ function extractReturnType(node) {
266
+ const annotation = node.returnType?.typeAnnotation ?? node.typeAnnotation?.typeAnnotation;
267
+ let baseType;
268
+ if (annotation) baseType = resolveTypeAnnotation(annotation);
269
+ else baseType = "void";
270
+ if (node.async && baseType !== "void") return `Promise<${baseType}>`;
271
+ if (node.async) return "Promise<void>";
272
+ return baseType;
273
+ }
274
+ function resolveTypeAnnotation(node) {
275
+ if (!node) return "unknown";
276
+ switch (node.type) {
277
+ case "TSStringKeyword": return "string";
278
+ case "TSNumberKeyword": return "number";
279
+ case "TSBooleanKeyword": return "boolean";
280
+ case "TSVoidKeyword": return "void";
281
+ case "TSAnyKeyword": return "any";
282
+ case "TSNullKeyword": return "null";
283
+ case "TSUndefinedKeyword": return "undefined";
284
+ case "TSObjectKeyword": return "object";
285
+ case "TSNeverKeyword": return "never";
286
+ case "TSUnknownKeyword": return "unknown";
287
+ case "TSTypeReference": {
288
+ const name = node.typeName?.type === "Identifier" ? node.typeName.name : node.typeName?.type === "TSQualifiedName" ? `${node.typeName.left?.name ?? ""}.${node.typeName.right?.name ?? ""}` : "unknown";
289
+ if (node.typeParameters?.params?.length > 0) return `${name}<${node.typeParameters.params.map((p) => resolveTypeAnnotation(p)).join(", ")}>`;
290
+ return name;
291
+ }
292
+ case "TSUnionType": return node.types.map((t) => resolveTypeAnnotation(t)).join(" | ");
293
+ case "TSIntersectionType": return node.types.map((t) => resolveTypeAnnotation(t)).join(" & ");
294
+ case "TSArrayType": return `${resolveTypeAnnotation(node.elementType)}[]`;
295
+ case "TSLiteralType":
296
+ if (node.literal.type === "StringLiteral") return `'${node.literal.value}'`;
297
+ if (node.literal.type === "NumericLiteral") return String(node.literal.value);
298
+ if (node.literal.type === "BooleanLiteral") return String(node.literal.value);
299
+ return "unknown";
300
+ case "TSFunctionType": return "Function";
301
+ case "TSTupleType": return `[${(node.elementTypes ?? []).map((t) => resolveTypeAnnotation(t)).join(", ")}]`;
302
+ case "TSParenthesizedType": return resolveTypeAnnotation(node.typeAnnotation);
303
+ case "TSTypeLiteral": return "object";
304
+ default: return "unknown";
305
+ }
306
+ }
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
5
358
  //#region src/parser.ts
6
359
  function parseJSDocTags(comments) {
7
360
  const result = { description: "" };
@@ -22,35 +375,61 @@ function parseJSDocTags(comments) {
22
375
  result.description = descLines.join(" ");
23
376
  return result;
24
377
  }
25
- function parseSFC(source, filename) {
378
+ function parseSFC(source, filename, sfcDir) {
26
379
  const doc = {
27
380
  name: filename.replace(/\.vue$/, "").split("/").pop() ?? "Unknown",
28
381
  props: [],
29
382
  emits: []
30
383
  };
31
- const { descriptor } = parse(source, { filename });
384
+ const fullPath = sfcDir ? `${sfcDir}/${filename}` : filename;
385
+ const { descriptor } = parse(source, { filename: fullPath });
386
+ doc.scriptSetup = !!descriptor.scriptSetup;
32
387
  if (descriptor.template?.ast) {
33
388
  const templateSlots = extractTemplateSlots(descriptor.template.ast);
34
389
  if (templateSlots.length > 0) doc.slots = templateSlots;
35
390
  }
36
391
  if (!descriptor.scriptSetup && !descriptor.script) return doc;
37
- const compiled = compileScript(descriptor, { id: filename });
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
+ }
38
410
  const componentJSDoc = extractComponentJSDoc(compiled.scriptSetupAst ?? compiled.scriptAst ?? []);
39
411
  doc.description = componentJSDoc.description;
40
412
  doc.internal = componentJSDoc.internal;
41
413
  const setupAst = compiled.scriptSetupAst;
42
414
  if (setupAst) {
43
415
  const scriptSource = descriptor.scriptSetup?.content ?? compiled.content;
416
+ const importMap = buildImportMap(setupAst);
44
417
  for (const stmt of setupAst) {
45
418
  const calls = extractDefineCalls(stmt);
46
419
  for (const { callee, args, leadingComments, typeParams, defaultsArg } of calls) if (callee === "defineProps" && args[0]?.type === "ObjectExpression") doc.props = extractProps(args[0], scriptSource);
47
420
  else if (callee === "defineProps" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.props = extractTypeProps(typeParams.params[0], defaultsArg, scriptSource);
48
- else if (callee === "defineEmits" && args[0]?.type === "ArrayExpression") doc.emits = extractEmits(args[0], leadingComments);
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);
49
428
  else if (callee === "defineEmits" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.emits = extractTypeEmits(typeParams.params[0]);
50
429
  else if (callee === "defineSlots" && typeParams?.params[0]?.type === "TSTypeLiteral") doc.slots = extractTypeSlots(typeParams.params[0]);
51
430
  else if (callee === "defineExpose" && args[0]?.type === "ObjectExpression") doc.exposes = extractExposes(args[0], scriptSource);
52
431
  }
53
- doc.composables = extractComposables(setupAst);
432
+ doc.composables = extractComposables(setupAst, importMap, sfcDir);
54
433
  }
55
434
  const scriptAst = compiled.scriptAst;
56
435
  if (scriptAst && doc.props.length === 0 && doc.emits.length === 0) {
@@ -331,26 +710,94 @@ function extractExposes(obj, _source) {
331
710
  }
332
711
  return exposes;
333
712
  }
334
- function extractComposables(ast) {
713
+ function buildImportMap(ast) {
714
+ const map = /* @__PURE__ */ new Map();
715
+ for (const stmt of ast) if (stmt.type === "ImportDeclaration") {
716
+ for (const spec of stmt.specifiers ?? []) if (spec.type === "ImportSpecifier" || spec.type === "ImportDefaultSpecifier") map.set(spec.local.name, stmt.source.value);
717
+ }
718
+ return map;
719
+ }
720
+ function extractVariablesFromPattern(decl) {
721
+ const id = decl.id;
722
+ if (!id) return [];
723
+ if (id.type === "Identifier") {
724
+ const v = { name: id.name };
725
+ if (id.typeAnnotation?.typeAnnotation) v.type = resolveTypeString(id.typeAnnotation.typeAnnotation);
726
+ return [v];
727
+ }
728
+ if (id.type === "ObjectPattern") {
729
+ const vars = [];
730
+ const typeAnnotation = id.typeAnnotation?.typeAnnotation;
731
+ const typeMembers = typeAnnotation?.type === "TSTypeLiteral" ? typeAnnotation.members : null;
732
+ for (const prop of id.properties) if (prop.type === "RestElement") {
733
+ const name = prop.argument?.name ?? "rest";
734
+ vars.push({ name });
735
+ } else if (prop.type === "ObjectProperty") {
736
+ 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 : "";
737
+ if (!name) continue;
738
+ const v = { name };
739
+ if (typeMembers) {
740
+ const keyName = prop.key?.type === "Identifier" ? prop.key.name : "";
741
+ for (const member of typeMembers) if (member.type === "TSPropertySignature" && member.key?.type === "Identifier" && member.key.name === keyName && member.typeAnnotation?.typeAnnotation) {
742
+ v.type = resolveTypeString(member.typeAnnotation.typeAnnotation);
743
+ break;
744
+ }
745
+ }
746
+ vars.push(v);
747
+ }
748
+ return vars;
749
+ }
750
+ if (id.type === "ArrayPattern") {
751
+ const vars = [];
752
+ for (const el of id.elements) {
753
+ if (!el) continue;
754
+ if (el.type === "Identifier") vars.push({ name: el.name });
755
+ else if (el.type === "RestElement" && el.argument?.type === "Identifier") vars.push({ name: el.argument.name });
756
+ else if (el.type === "AssignmentPattern" && el.left?.type === "Identifier") vars.push({ name: el.left.name });
757
+ }
758
+ return vars;
759
+ }
760
+ return [];
761
+ }
762
+ function extractComposables(ast, importMap, sfcDir) {
335
763
  const seen = /* @__PURE__ */ new Set();
336
764
  const composables = [];
337
765
  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 });
766
+ if (stmt.type === "ExpressionStatement" && stmt.expression.type === "CallExpression" && stmt.expression.callee.type === "Identifier" && /^use[A-Z]/.test(stmt.expression.callee.name)) {
767
+ const name = stmt.expression.callee.name;
768
+ if (!seen.has(name)) {
769
+ seen.add(name);
770
+ composables.push({
771
+ name,
772
+ source: importMap.get(name),
773
+ variables: []
774
+ });
775
+ }
776
+ }
777
+ if (stmt.type === "VariableDeclaration") {
778
+ for (const decl of stmt.declarations) if (decl.init?.type === "CallExpression" && decl.init.callee.type === "Identifier" && /^use[A-Z]/.test(decl.init.callee.name)) {
779
+ const name = decl.init.callee.name;
780
+ if (seen.has(name)) continue;
781
+ seen.add(name);
782
+ const variables = extractVariablesFromPattern(decl);
783
+ const source = importMap.get(name);
784
+ if (variables.some((v) => !v.type) && sfcDir && source) {
785
+ const resolvedPath = resolveImportPath(source, sfcDir);
786
+ if (resolvedPath) {
787
+ const typeMap = resolveComposableTypes(resolvedPath, name, variables.filter((v) => !v.type).map((v) => v.name));
788
+ for (const v of variables) if (!v.type && typeMap.has(v.name)) v.type = typeMap.get(v.name);
789
+ }
790
+ }
791
+ composables.push({
792
+ name,
793
+ source,
794
+ variables
795
+ });
796
+ }
342
797
  }
343
798
  }
344
799
  return composables;
345
800
  }
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
801
  function extractTemplateSlots(templateAst) {
355
802
  const slots = [];
356
803
  walkTemplate(templateAst.children ?? [], slots);
@@ -448,9 +895,13 @@ function stringifyDefault(node, source) {
448
895
  function esc(value) {
449
896
  return value.replaceAll("|", "\\|");
450
897
  }
898
+ function escHtml(value) {
899
+ return value.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
900
+ }
451
901
  function generateMarkdown(doc) {
452
902
  const sections = [`# ${doc.name}`];
453
903
  if (doc.description) sections.push("", doc.description);
904
+ if (doc.scriptSetup) sections.push("", "**Note:** Uses `<script setup>` syntax.");
454
905
  const hasProps = doc.props.length > 0;
455
906
  const hasEmits = doc.emits.length > 0;
456
907
  const hasSlots = (doc.slots?.length ?? 0) > 0;
@@ -518,49 +969,273 @@ function generateMarkdown(doc) {
518
969
  }
519
970
  }
520
971
  if (hasComposables) {
521
- sections.push("", "## Composables Used", "");
522
- for (const c of doc.composables) sections.push(`- \`${c.name}\``);
972
+ sections.push("", "## Composables Used");
973
+ for (const c of doc.composables) {
974
+ sections.push("", `### \`${c.name}\``);
975
+ if (c.source && (c.source.startsWith(".") || c.source.startsWith("@/"))) sections.push("", `*Source: \`${c.source}\`*`);
976
+ if (c.variables.length === 0) sections.push("", "Called for side effects.");
977
+ else if (!c.variables.some((v) => v.type) && c.variables.length <= 3) {
978
+ const vars = c.variables.map((v) => `\`${v.name}\``).join(", ");
979
+ sections.push("", `**Returns:** ${vars}`);
980
+ } else {
981
+ sections.push("");
982
+ sections.push("| Variable | Type |");
983
+ sections.push("| --- | --- |");
984
+ for (const v of c.variables) {
985
+ const type = v.type ? escHtml(esc(v.type)) : "-";
986
+ sections.push(`| ${esc(v.name)} | ${type} |`);
987
+ }
988
+ }
989
+ }
523
990
  }
524
991
  return sections.join("\n") + "\n";
525
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
+ }
526
999
  //#endregion
527
1000
  //#region src/index.ts
528
1001
  function parseComponent(filePath) {
529
1002
  const abs = resolve(filePath);
530
- return parseSFC(readFileSync(abs, "utf-8"), abs.split("/").pop() ?? "Unknown.vue");
1003
+ return parseSFC(readFileSync(abs, "utf-8"), abs.split("/").pop() ?? "Unknown.vue", abs.substring(0, abs.lastIndexOf("/")));
531
1004
  }
532
1005
  //#endregion
533
- //#region src/cli.ts
534
- const filePath = process.argv[2];
535
- if (!filePath) {
536
- console.error("Usage: compmark <path-to-component.vue>");
537
- process.exit(1);
538
- }
539
- if (!filePath.endsWith(".vue")) {
540
- console.error(`Error: Expected a .vue file, got: ${filePath}`);
541
- process.exit(1);
542
- }
543
- const abs = resolve(filePath);
544
- if (!existsSync(abs)) {
545
- console.error(`Error: File not found: ${filePath}`);
546
- process.exit(1);
547
- }
548
- try {
549
- const doc = parseComponent(abs);
550
- if (doc.internal) {
551
- const name = abs.split("/").pop() ?? filePath;
552
- console.log(`Skipped ${name} (marked @internal)`);
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();
553
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
+ }
554
1236
  }
555
- const md = generateMarkdown(doc);
556
- const outFile = `${doc.name}.md`;
557
- writeFileSync(join(process.cwd(), outFile), md, "utf-8");
558
- console.log(`Created ${outFile}`);
559
- } catch (err) {
560
- const name = abs.split("/").pop() ?? filePath;
561
- const reason = err instanceof Error ? err.message : String(err);
562
- console.error(`Error: Could not parse ${name}: ${reason}`);
563
- process.exit(1);
1237
+ return paths;
564
1238
  }
1239
+ runMain(main);
565
1240
  //#endregion
566
1241
  export {};