compmark-vue 0.3.3 → 0.4.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,7 @@
1
1
  #!/usr/bin/env node
2
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";
3
+ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
5
  import { glob } from "tinyglobby";
6
6
  import { babelParse, compileScript, parse } from "@vue/compiler-sfc";
7
7
  //#region src/discovery.ts
@@ -9,17 +9,44 @@ async function discoverFiles(inputs, ignore) {
9
9
  const baseIgnore = ["**/node_modules/**"];
10
10
  const userIgnore = (ignore ?? []).map(normalizeIgnorePattern);
11
11
  const allIgnore = [...baseIgnore, ...userIgnore];
12
+ const hasUserIgnore = userIgnore.length > 0;
12
13
  const found = /* @__PURE__ */ new Set();
14
+ let ignoredCount = 0;
13
15
  for (const input of inputs) {
14
16
  const resolved = resolve(input);
15
17
  if (input.endsWith(".vue") && existsSync(resolved)) found.add(resolved);
16
- else if (isDirectory$1(resolved)) {
18
+ else if (isDirectory$1(resolved)) if (hasUserIgnore) {
19
+ const allFiles = await glob("**/*.vue", {
20
+ cwd: resolved,
21
+ absolute: true,
22
+ ignore: baseIgnore
23
+ });
24
+ const filteredFiles = await glob("**/*.vue", {
25
+ cwd: resolved,
26
+ absolute: true,
27
+ ignore: allIgnore
28
+ });
29
+ ignoredCount += allFiles.length - filteredFiles.length;
30
+ for (const f of filteredFiles) found.add(f);
31
+ } else {
17
32
  const files = await glob("**/*.vue", {
18
33
  cwd: resolved,
19
34
  absolute: true,
20
35
  ignore: allIgnore
21
36
  });
22
37
  for (const f of files) found.add(f);
38
+ }
39
+ else if (hasUserIgnore) {
40
+ const allFiles = await glob(input, {
41
+ absolute: true,
42
+ ignore: baseIgnore
43
+ });
44
+ const filteredFiles = await glob(input, {
45
+ absolute: true,
46
+ ignore: allIgnore
47
+ });
48
+ ignoredCount += allFiles.length - filteredFiles.length;
49
+ for (const f of filteredFiles) found.add(f);
23
50
  } else {
24
51
  const files = await glob(input, {
25
52
  absolute: true,
@@ -28,7 +55,25 @@ async function discoverFiles(inputs, ignore) {
28
55
  for (const f of files) found.add(f);
29
56
  }
30
57
  }
31
- return [...found].sort();
58
+ const files = [...found].sort();
59
+ const basePath = computeBasePath(files);
60
+ return {
61
+ files,
62
+ ignoredCount,
63
+ basePath
64
+ };
65
+ }
66
+ function computeBasePath(files) {
67
+ if (files.length === 0) return ".";
68
+ if (files.length === 1) return dirname(files[0]);
69
+ const parts = files.map((f) => dirname(f).split("/"));
70
+ const common = [];
71
+ for (let i = 0; i < parts[0].length; i++) {
72
+ const segment = parts[0][i];
73
+ if (parts.every((p) => p[i] === segment)) common.push(segment);
74
+ else break;
75
+ }
76
+ return common.join("/") || "/";
32
77
  }
33
78
  function isDirectory$1(path) {
34
79
  try {
@@ -209,15 +254,15 @@ function inferType(node) {
209
254
  if (typeParams?.params?.length > 0) return `Ref<${resolveTypeAnnotation(typeParams.params[0])}>`;
210
255
  const arg = node.arguments[0];
211
256
  if (!arg) return "Ref<unknown>";
212
- return `Ref<${inferLiteralType(arg)}>`;
257
+ return `Ref<${inferLiteralType$1(arg)}>`;
213
258
  }
214
259
  if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "computed") return "ComputedRef";
215
260
  if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "reactive") return "Object";
216
261
  if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") return inferFunctionSignature(node);
217
262
  if (node.type === "CallExpression" && node.callee.type === "Identifier" && /^use[A-Z]/.test(node.callee.name)) return "unknown";
218
- return inferLiteralType(node);
263
+ return inferLiteralType$1(node);
219
264
  }
220
- function inferLiteralType(node) {
265
+ function inferLiteralType$1(node) {
221
266
  switch (node.type) {
222
267
  case "NumericLiteral": return "number";
223
268
  case "StringLiteral": return "string";
@@ -430,12 +475,17 @@ function parseSFC(source, filename, sfcDir) {
430
475
  else if (callee === "defineExpose" && args[0]?.type === "ObjectExpression") doc.exposes = extractExposes(args[0], scriptSource);
431
476
  }
432
477
  doc.composables = extractComposables(setupAst, importMap, sfcDir);
478
+ const { refs, computeds } = extractRefsAndComputeds(setupAst, scriptSource);
479
+ if (refs.length > 0) doc.refs = refs;
480
+ if (computeds.length > 0) doc.computeds = computeds;
433
481
  }
434
482
  const scriptAst = compiled.scriptAst;
435
483
  if (scriptAst && doc.props.length === 0 && doc.emits.length === 0) {
436
484
  const optionsDoc = extractOptionsAPI(scriptAst, compiled.content);
437
485
  doc.props = optionsDoc.props;
438
486
  doc.emits = optionsDoc.emits;
487
+ if (optionsDoc.refs.length > 0) doc.refs = optionsDoc.refs;
488
+ if (optionsDoc.computeds.length > 0) doc.computeds = optionsDoc.computeds;
439
489
  }
440
490
  return doc;
441
491
  }
@@ -444,7 +494,8 @@ function extractComponentJSDoc(ast) {
444
494
  description: "",
445
495
  internal: false
446
496
  };
447
- const comments = ast[0].leadingComments ?? [];
497
+ const first = ast[0];
498
+ const comments = first.leadingComments ?? [];
448
499
  if (comments.length === 0) return {
449
500
  description: "",
450
501
  internal: false
@@ -459,6 +510,13 @@ function extractComponentJSDoc(ast) {
459
510
  description: result.description,
460
511
  internal: true
461
512
  };
513
+ const isImport = first.type === "ImportDeclaration";
514
+ const isDefineCall = first.type === "ExpressionStatement" && first.expression.type === "CallExpression" && first.expression.callee.type === "Identifier" && first.expression.callee.name.startsWith("define");
515
+ const isDefineVar = first.type === "VariableDeclaration" && first.declarations.some((d) => d.init?.type === "CallExpression" && d.init.callee?.type === "Identifier" && (d.init.callee.name.startsWith("define") || d.init.callee.name === "withDefaults"));
516
+ if (firstComment.value.includes("@component") || isImport || isDefineCall || isDefineVar) return {
517
+ description: result.description,
518
+ internal: false
519
+ };
462
520
  return {
463
521
  description: "",
464
522
  internal: false
@@ -798,6 +856,99 @@ function extractComposables(ast, importMap, sfcDir) {
798
856
  }
799
857
  return composables;
800
858
  }
859
+ const REF_CALLEES = new Set([
860
+ "ref",
861
+ "shallowRef",
862
+ "reactive",
863
+ "shallowReactive"
864
+ ]);
865
+ const COMPUTED_CALLEES = new Set(["computed"]);
866
+ const SKIP_CALLEES = new Set([
867
+ "defineProps",
868
+ "defineEmits",
869
+ "defineSlots",
870
+ "defineExpose",
871
+ "withDefaults"
872
+ ]);
873
+ function inferLiteralType(node) {
874
+ switch (node.type) {
875
+ case "StringLiteral": return "string";
876
+ case "NumericLiteral": return "number";
877
+ case "BooleanLiteral": return "boolean";
878
+ case "ArrayExpression": return "Array";
879
+ case "ObjectExpression": return "Object";
880
+ case "NullLiteral": return "null";
881
+ default: return null;
882
+ }
883
+ }
884
+ function extractRefsAndComputeds(ast, _scriptSource) {
885
+ const refs = [];
886
+ const computeds = [];
887
+ for (const stmt of ast) {
888
+ if (stmt.type !== "VariableDeclaration") continue;
889
+ const comments = stmt.leadingComments ?? [];
890
+ for (const decl of stmt.declarations) {
891
+ if (!decl.init || decl.init.type !== "CallExpression") continue;
892
+ if (decl.init.callee.type !== "Identifier") continue;
893
+ const calleeName = decl.init.callee.name;
894
+ if (/^use[A-Z]/.test(calleeName)) continue;
895
+ if (SKIP_CALLEES.has(calleeName)) continue;
896
+ const isRef = REF_CALLEES.has(calleeName);
897
+ const isComputed = COMPUTED_CALLEES.has(calleeName);
898
+ if (!isRef && !isComputed) continue;
899
+ const id = decl.id;
900
+ if (!id || id.type !== "Identifier") continue;
901
+ const varName = id.name;
902
+ const jsdoc = parseJSDocTags(comments);
903
+ const callExpr = decl.init;
904
+ const typeParams = callExpr.typeParameters;
905
+ const args = callExpr.arguments;
906
+ let type;
907
+ if (id.typeAnnotation?.typeAnnotation) type = resolveTypeString(id.typeAnnotation.typeAnnotation);
908
+ else if (typeParams?.params?.[0]) {
909
+ const genericType = resolveTypeString(typeParams.params[0]);
910
+ if (calleeName === "reactive" || calleeName === "shallowReactive") type = genericType;
911
+ else if (calleeName === "shallowRef") type = `ShallowRef<${genericType}>`;
912
+ else if (calleeName === "computed") type = `ComputedRef<${genericType}>`;
913
+ else type = `Ref<${genericType}>`;
914
+ } else if (isRef && args[0]) {
915
+ const literalType = inferLiteralType(args[0]);
916
+ if (literalType) if (calleeName === "reactive" || calleeName === "shallowReactive") type = literalType;
917
+ else if (calleeName === "shallowRef") type = `ShallowRef<${literalType}>`;
918
+ else type = `Ref<${literalType}>`;
919
+ else if (calleeName === "reactive") type = "Object";
920
+ else if (calleeName === "shallowReactive") type = "Object";
921
+ else if (calleeName === "shallowRef") type = "ShallowRef";
922
+ else type = "Ref";
923
+ } else if (isComputed && args[0]) {
924
+ let returnType = null;
925
+ const getter = args[0];
926
+ if ((getter.type === "ArrowFunctionExpression" || getter.type === "FunctionExpression") && getter.returnType?.type === "TSTypeAnnotation") returnType = resolveTypeString(getter.returnType.typeAnnotation);
927
+ if (returnType) type = `ComputedRef<${returnType}>`;
928
+ else type = "ComputedRef";
929
+ } else if (calleeName === "reactive") type = "Object";
930
+ else if (calleeName === "shallowReactive") type = "Object";
931
+ else if (calleeName === "shallowRef") type = "ShallowRef";
932
+ else if (calleeName === "computed") type = "ComputedRef";
933
+ else type = "Ref";
934
+ const docEntry = {
935
+ name: varName,
936
+ type,
937
+ description: jsdoc.description,
938
+ ...jsdoc.deprecated !== void 0 && { deprecated: jsdoc.deprecated },
939
+ ...jsdoc.since && { since: jsdoc.since },
940
+ ...jsdoc.example && { example: jsdoc.example },
941
+ ...jsdoc.see && { see: jsdoc.see }
942
+ };
943
+ if (isRef) refs.push(docEntry);
944
+ else computeds.push(docEntry);
945
+ }
946
+ }
947
+ return {
948
+ refs,
949
+ computeds
950
+ };
951
+ }
801
952
  function extractTemplateSlots(templateAst) {
802
953
  const slots = [];
803
954
  walkTemplate(templateAst.children ?? [], slots);
@@ -827,31 +978,110 @@ function walkTemplate(children, slots) {
827
978
  function extractOptionsAPI(ast, source) {
828
979
  let props = [];
829
980
  let emits = [];
981
+ let refs = [];
982
+ let computeds = [];
830
983
  for (const stmt of ast) {
831
984
  if (stmt.type !== "ExportDefaultDeclaration") continue;
832
985
  const decl = stmt.declaration;
833
986
  if (decl.type !== "ObjectExpression") continue;
834
987
  for (const prop of decl.properties) {
835
- if (prop.type !== "ObjectProperty") continue;
988
+ if (prop.type !== "ObjectProperty" && prop.type !== "ObjectMethod") continue;
836
989
  const key = prop.key;
837
990
  const name = key.type === "Identifier" ? key.name : "";
838
991
  if (name === "props") {
992
+ if (prop.type !== "ObjectProperty") continue;
839
993
  if (prop.value.type === "ObjectExpression") props = extractProps(prop.value, source);
840
994
  else if (prop.value.type === "ArrayExpression") props = extractArrayProps(prop.value);
841
995
  } else if (name === "emits") {
996
+ if (prop.type !== "ObjectProperty") continue;
842
997
  if (prop.value.type === "ArrayExpression") {
843
998
  const comments = stmt.leadingComments ?? [];
844
999
  emits = extractEmits(prop.value, comments);
845
1000
  } else if (prop.value.type === "ObjectExpression") emits = extractObjectEmits(prop.value);
1001
+ } else if (name === "data") refs = extractOptionsData(prop);
1002
+ else if (name === "computed") {
1003
+ if (prop.type !== "ObjectProperty") continue;
1004
+ if (prop.value.type === "ObjectExpression") computeds = extractOptionsComputed(prop.value);
846
1005
  }
847
1006
  }
848
1007
  break;
849
1008
  }
850
1009
  return {
851
1010
  props,
852
- emits
1011
+ emits,
1012
+ refs,
1013
+ computeds
853
1014
  };
854
1015
  }
1016
+ function extractOptionsData(prop) {
1017
+ const refs = [];
1018
+ let body = null;
1019
+ if (prop.type === "ObjectMethod") body = prop.body;
1020
+ else if (prop.type === "ObjectProperty") {
1021
+ const val = prop.value;
1022
+ if (val.type === "ArrowFunctionExpression" || val.type === "FunctionExpression") body = val.body;
1023
+ }
1024
+ if (!body) return refs;
1025
+ let returnArg = null;
1026
+ if (body.type === "ObjectExpression") returnArg = body;
1027
+ else if (body.type === "BlockStatement") {
1028
+ for (const s of body.body) if (s.type === "ReturnStatement" && s.argument?.type === "ObjectExpression") {
1029
+ returnArg = s.argument;
1030
+ break;
1031
+ }
1032
+ }
1033
+ if (!returnArg || returnArg.type !== "ObjectExpression") return refs;
1034
+ for (const p of returnArg.properties) {
1035
+ if (p.type !== "ObjectProperty") continue;
1036
+ const name = p.key.type === "Identifier" ? p.key.name : p.key.type === "StringLiteral" ? p.key.value : "";
1037
+ if (!name) continue;
1038
+ const jsdoc = parseJSDocTags(p.leadingComments ?? []);
1039
+ const type = inferLiteralType(p.value) ?? "unknown";
1040
+ refs.push({
1041
+ name,
1042
+ type,
1043
+ description: jsdoc.description,
1044
+ ...jsdoc.deprecated !== void 0 && { deprecated: jsdoc.deprecated },
1045
+ ...jsdoc.since && { since: jsdoc.since },
1046
+ ...jsdoc.example && { example: jsdoc.example },
1047
+ ...jsdoc.see && { see: jsdoc.see }
1048
+ });
1049
+ }
1050
+ return refs;
1051
+ }
1052
+ function extractOptionsComputed(obj) {
1053
+ const computeds = [];
1054
+ for (const prop of obj.properties) {
1055
+ if (prop.type !== "ObjectProperty" && prop.type !== "ObjectMethod") continue;
1056
+ const name = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : "";
1057
+ if (!name) continue;
1058
+ const jsdoc = parseJSDocTags(prop.leadingComments ?? []);
1059
+ let type = "unknown";
1060
+ if (prop.type === "ObjectMethod") {
1061
+ if (prop.returnType?.type === "TSTypeAnnotation") type = resolveTypeString(prop.returnType.typeAnnotation);
1062
+ } else if (prop.type === "ObjectProperty") {
1063
+ const val = prop.value;
1064
+ if (val.type === "ObjectExpression") {
1065
+ for (const member of val.properties) if (member.type === "ObjectMethod" && member.key?.type === "Identifier" && member.key.name === "get") {
1066
+ if (member.returnType?.type === "TSTypeAnnotation") type = resolveTypeString(member.returnType.typeAnnotation);
1067
+ break;
1068
+ }
1069
+ } else if (val.type === "ArrowFunctionExpression" || val.type === "FunctionExpression") {
1070
+ if (val.returnType?.type === "TSTypeAnnotation") type = resolveTypeString(val.returnType.typeAnnotation);
1071
+ }
1072
+ }
1073
+ computeds.push({
1074
+ name,
1075
+ type,
1076
+ description: jsdoc.description,
1077
+ ...jsdoc.deprecated !== void 0 && { deprecated: jsdoc.deprecated },
1078
+ ...jsdoc.since && { since: jsdoc.since },
1079
+ ...jsdoc.example && { example: jsdoc.example },
1080
+ ...jsdoc.see && { see: jsdoc.see }
1081
+ });
1082
+ }
1083
+ return computeds;
1084
+ }
855
1085
  function extractArrayProps(arr) {
856
1086
  const props = [];
857
1087
  for (const el of arr.elements) if (el?.type === "StringLiteral") props.push({
@@ -902,15 +1132,53 @@ function generateMarkdown(doc) {
902
1132
  const sections = [`# ${doc.name}`];
903
1133
  if (doc.description) sections.push("", doc.description);
904
1134
  if (doc.scriptSetup) sections.push("", "**Note:** Uses `<script setup>` syntax.");
1135
+ const hasRefs = (doc.refs?.length ?? 0) > 0;
1136
+ const hasComputeds = (doc.computeds?.length ?? 0) > 0;
905
1137
  const hasProps = doc.props.length > 0;
906
1138
  const hasEmits = doc.emits.length > 0;
907
1139
  const hasSlots = (doc.slots?.length ?? 0) > 0;
908
1140
  const hasExposes = (doc.exposes?.length ?? 0) > 0;
909
1141
  const hasComposables = (doc.composables?.length ?? 0) > 0;
910
- if (!hasProps && !hasEmits && !hasSlots && !hasExposes && !hasComposables) {
1142
+ if (!hasProps && !hasEmits && !hasSlots && !hasExposes && !hasComposables && !hasRefs && !hasComputeds) {
911
1143
  sections.push("", "No documentable API found.");
912
1144
  return sections.join("\n") + "\n";
913
1145
  }
1146
+ if (hasRefs) {
1147
+ sections.push("", "## Refs", "");
1148
+ sections.push("| Name | Type | Description |");
1149
+ sections.push("| --- | --- | --- |");
1150
+ const examples = [];
1151
+ for (const r of doc.refs) {
1152
+ let desc = r.description || "-";
1153
+ if (r.deprecated) desc += typeof r.deprecated === "string" && r.deprecated ? ` **Deprecated**: ${r.deprecated}` : " **Deprecated**";
1154
+ if (r.since) desc += ` *(since ${r.since})*`;
1155
+ if (r.see) desc += ` See: ${r.see}`;
1156
+ sections.push(`| ${esc(r.name)} | ${escHtml(esc(r.type))} | ${esc(desc)} |`);
1157
+ if (r.example) examples.push({
1158
+ name: r.name,
1159
+ example: r.example
1160
+ });
1161
+ }
1162
+ for (const { name, example } of examples) sections.push("", `**\`${name}\` example:**`, "", "```", example, "```");
1163
+ }
1164
+ if (hasComputeds) {
1165
+ sections.push("", "## Computed", "");
1166
+ sections.push("| Name | Type | Description |");
1167
+ sections.push("| --- | --- | --- |");
1168
+ const examples = [];
1169
+ for (const c of doc.computeds) {
1170
+ let desc = c.description || "-";
1171
+ if (c.deprecated) desc += typeof c.deprecated === "string" && c.deprecated ? ` **Deprecated**: ${c.deprecated}` : " **Deprecated**";
1172
+ if (c.since) desc += ` *(since ${c.since})*`;
1173
+ if (c.see) desc += ` See: ${c.see}`;
1174
+ sections.push(`| ${esc(c.name)} | ${escHtml(esc(c.type))} | ${esc(desc)} |`);
1175
+ if (c.example) examples.push({
1176
+ name: c.name,
1177
+ example: c.example
1178
+ });
1179
+ }
1180
+ for (const { name, example } of examples) sections.push("", `**\`${name}\` example:**`, "", "```", example, "```");
1181
+ }
914
1182
  if (hasProps) {
915
1183
  sections.push("", "## Props", "");
916
1184
  sections.push("| Name | Type | Required | Default | Description |");
@@ -1041,18 +1309,28 @@ function processFiles(filePaths, options) {
1041
1309
  }
1042
1310
  //#endregion
1043
1311
  //#region src/output.ts
1044
- function writeIndividualMarkdown(results, outDir, silent) {
1312
+ function writeIndividualMarkdown(results, outDir, silent, options) {
1045
1313
  mkdirSync(outDir, { recursive: true });
1314
+ const outputMap = /* @__PURE__ */ new Map();
1046
1315
  const usedNames = /* @__PURE__ */ new Map();
1047
- for (const { doc } of results) {
1316
+ for (const { path: sourcePath, doc } of results) {
1317
+ let targetDir = outDir;
1318
+ if (options?.preserveStructure && options.basePath) {
1319
+ targetDir = join(outDir, relative(options.basePath, dirname(sourcePath)));
1320
+ mkdirSync(targetDir, { recursive: true });
1321
+ }
1048
1322
  const baseName = doc.name;
1049
- const count = usedNames.get(baseName) ?? 0;
1050
- usedNames.set(baseName, count + 1);
1323
+ const dedupKey = options?.preserveStructure ? `${targetDir}/${baseName}` : baseName;
1324
+ const count = usedNames.get(dedupKey) ?? 0;
1325
+ usedNames.set(dedupKey, count + 1);
1051
1326
  const fileName = count === 0 ? baseName : `${baseName}-${count + 1}`;
1052
1327
  const md = generateMarkdown(doc);
1053
- writeFileSync(join(outDir, `${fileName}.md`), md, "utf-8");
1328
+ const outPath = join(targetDir, `${fileName}.md`);
1329
+ writeFileSync(outPath, md, "utf-8");
1330
+ outputMap.set(sourcePath, outPath);
1054
1331
  if (!silent) console.log(` Created ${fileName}.md`);
1055
1332
  }
1333
+ return outputMap;
1056
1334
  }
1057
1335
  function writeJoinedMarkdown(results, outDir, silent) {
1058
1336
  mkdirSync(outDir, { recursive: true });
@@ -1077,8 +1355,9 @@ function writeJoinedMarkdown(results, outDir, silent) {
1077
1355
  writeFileSync(join(outDir, "components.md"), sections.join("\n") + "\n", "utf-8");
1078
1356
  if (!silent) console.log(` Created components.md`);
1079
1357
  }
1080
- function writeJSON(results, outDir, joined, silent) {
1358
+ function writeJSON(results, outDir, joined, silent, options) {
1081
1359
  mkdirSync(outDir, { recursive: true });
1360
+ const outputMap = /* @__PURE__ */ new Map();
1082
1361
  if (joined) {
1083
1362
  const data = {
1084
1363
  generated: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1088,42 +1367,82 @@ function writeJSON(results, outDir, joined, silent) {
1088
1367
  if (!silent) console.log(` Created components.json`);
1089
1368
  } else {
1090
1369
  const usedNames = /* @__PURE__ */ new Map();
1091
- for (const { doc } of results) {
1370
+ for (const { path: sourcePath, doc } of results) {
1371
+ let targetDir = outDir;
1372
+ if (options?.preserveStructure && options.basePath) {
1373
+ targetDir = join(outDir, relative(options.basePath, dirname(sourcePath)));
1374
+ mkdirSync(targetDir, { recursive: true });
1375
+ }
1092
1376
  const baseName = doc.name;
1093
- const count = usedNames.get(baseName) ?? 0;
1094
- usedNames.set(baseName, count + 1);
1377
+ const dedupKey = options?.preserveStructure ? `${targetDir}/${baseName}` : baseName;
1378
+ const count = usedNames.get(dedupKey) ?? 0;
1379
+ usedNames.set(dedupKey, count + 1);
1095
1380
  const fileName = count === 0 ? baseName : `${baseName}-${count + 1}`;
1096
- writeFileSync(join(outDir, `${fileName}.json`), JSON.stringify(doc, null, 2) + "\n", "utf-8");
1381
+ const outPath = join(targetDir, `${fileName}.json`);
1382
+ writeFileSync(outPath, JSON.stringify(doc, null, 2) + "\n", "utf-8");
1383
+ outputMap.set(sourcePath, outPath);
1097
1384
  if (!silent) console.log(` Created ${fileName}.json`);
1098
1385
  }
1099
1386
  }
1387
+ return outputMap;
1100
1388
  }
1101
1389
  //#endregion
1102
1390
  //#region src/watcher.ts
1103
- function startWatcher(inputs, ignore, rebuild) {
1391
+ function startWatcher(inputs, ignore, rebuildOrOptions) {
1392
+ const options = typeof rebuildOrOptions === "function" ? {
1393
+ rebuild: rebuildOrOptions,
1394
+ isJoined: true
1395
+ } : rebuildOrOptions;
1104
1396
  const roots = /* @__PURE__ */ new Set();
1105
1397
  for (const input of inputs) if (input.endsWith(".vue")) roots.add(dirname(resolve(input)));
1106
1398
  else roots.add(resolve(input));
1107
1399
  let timer = null;
1108
- const debounce = () => {
1109
- if (timer) clearTimeout(timer);
1110
- timer = setTimeout(() => {
1400
+ const changedFiles = /* @__PURE__ */ new Set();
1401
+ const flush = async () => {
1402
+ if (options.isJoined || !options.incrementalUpdate) {
1111
1403
  console.log("[watch] Rebuilding...");
1112
1404
  try {
1113
- rebuild();
1405
+ await options.rebuild();
1114
1406
  } catch (err) {
1115
1407
  const msg = err instanceof Error ? err.message : String(err);
1116
1408
  console.error(`[watch] Error: ${msg}`);
1117
1409
  }
1118
1410
  console.log("[watch] Done.");
1411
+ } else {
1412
+ const files = [...changedFiles];
1413
+ changedFiles.clear();
1414
+ for (const file of files) try {
1415
+ if (existsSync(file)) if (ignore.some((pattern) => file.includes(pattern))) {
1416
+ if (options.removeOutput) options.removeOutput(file);
1417
+ console.log(`[watch] Ignored: ${file.split("/").pop()}`);
1418
+ } else {
1419
+ console.log(`[watch] Updating: ${file.split("/").pop()}`);
1420
+ await options.incrementalUpdate(file);
1421
+ }
1422
+ else {
1423
+ if (options.removeOutput) options.removeOutput(file);
1424
+ console.log(`[watch] Removed: ${file.split("/").pop()}`);
1425
+ }
1426
+ } catch (err) {
1427
+ const msg = err instanceof Error ? err.message : String(err);
1428
+ console.error(`[watch] Error processing ${file.split("/").pop()}: ${msg}`);
1429
+ }
1430
+ console.log("[watch] Done.");
1431
+ }
1432
+ };
1433
+ const debounce = (filePath) => {
1434
+ if (filePath) changedFiles.add(filePath);
1435
+ if (timer) clearTimeout(timer);
1436
+ timer = setTimeout(() => {
1437
+ flush();
1119
1438
  }, 300);
1120
1439
  };
1121
1440
  const watchers = [];
1122
1441
  for (const root of roots) try {
1123
1442
  const watcher = watch(root, { recursive: true }, (_event, filename) => {
1124
1443
  if (!filename || !filename.endsWith(".vue")) return;
1125
- if (ignore.some((pattern) => filename.includes(pattern))) return;
1126
- debounce();
1444
+ if (ignore.some((pattern) => filename.includes(pattern)) && options.isJoined) return;
1445
+ debounce(join(root, filename));
1127
1446
  });
1128
1447
  watchers.push(watcher);
1129
1448
  } catch {
@@ -1135,15 +1454,27 @@ function startWatcher(inputs, ignore, rebuild) {
1135
1454
  process.exit(0);
1136
1455
  });
1137
1456
  }
1457
+ function removeOutputFile(outputMap, sourcePath) {
1458
+ const outPath = outputMap.get(sourcePath);
1459
+ if (outPath && existsSync(outPath)) {
1460
+ unlinkSync(outPath);
1461
+ outputMap.delete(sourcePath);
1462
+ }
1463
+ }
1138
1464
  //#endregion
1139
1465
  //#region src/cli.ts
1140
1466
  const main = defineCommand({
1141
1467
  meta: {
1142
1468
  name: "compmark",
1143
- version: "0.3.0",
1469
+ version: "0.4.0",
1144
1470
  description: "Auto-generate Markdown documentation from Vue 3 SFCs"
1145
1471
  },
1146
1472
  args: {
1473
+ paths: {
1474
+ type: "positional",
1475
+ description: "Files, directories, or glob patterns",
1476
+ required: true
1477
+ },
1147
1478
  out: {
1148
1479
  type: "string",
1149
1480
  description: "Output directory",
@@ -1169,6 +1500,10 @@ const main = defineCommand({
1169
1500
  silent: {
1170
1501
  type: "boolean",
1171
1502
  description: "Suppress non-error output"
1503
+ },
1504
+ "preserve-structure": {
1505
+ type: "boolean",
1506
+ description: "Preserve input folder structure in output"
1172
1507
  }
1173
1508
  },
1174
1509
  async run({ args }) {
@@ -1187,8 +1522,12 @@ const main = defineCommand({
1187
1522
  const silent = args.silent ?? false;
1188
1523
  const joined = args.join ?? false;
1189
1524
  const outDir = args.out ?? ".";
1525
+ const preserveStructure = args["preserve-structure"] ?? false;
1526
+ let outputMap = /* @__PURE__ */ new Map();
1527
+ let lastBasePath = "";
1190
1528
  const rebuild = async () => {
1191
- const filePaths = await discoverFiles(inputPaths, ignorePatterns);
1529
+ const { files: filePaths, ignoredCount, basePath } = await discoverFiles(inputPaths, ignorePatterns);
1530
+ lastBasePath = basePath;
1192
1531
  if (filePaths.length === 0) {
1193
1532
  if (!args.watch) {
1194
1533
  console.error("Error: No .vue files found");
@@ -1198,17 +1537,54 @@ const main = defineCommand({
1198
1537
  return null;
1199
1538
  }
1200
1539
  const summary = processFiles(filePaths, { silent });
1201
- if (format === "json") writeJSON(summary.files, outDir, joined, silent);
1540
+ const outputOptions = {
1541
+ preserveStructure,
1542
+ basePath
1543
+ };
1544
+ if (format === "json") outputMap = writeJSON(summary.files, outDir, joined, silent, outputOptions);
1202
1545
  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`);
1546
+ else outputMap = writeIndividualMarkdown(summary.files, outDir, silent, outputOptions);
1547
+ const totalSkipped = summary.skipped + ignoredCount;
1548
+ if (!silent) console.log(`✓ ${summary.documented} components documented, ${totalSkipped} skipped, ${summary.errors} errors`);
1205
1549
  return summary;
1206
1550
  };
1207
1551
  const summary = await rebuild();
1208
- if (args.watch) startWatcher(inputPaths, ignorePatterns, () => {
1209
- rebuild();
1210
- });
1211
- else if (summary && summary.errors > 0) process.exit(1);
1552
+ if (args.watch) {
1553
+ const outputOptions = {
1554
+ preserveStructure,
1555
+ basePath: lastBasePath
1556
+ };
1557
+ startWatcher(inputPaths, ignorePatterns, {
1558
+ rebuild: async () => {
1559
+ await rebuild();
1560
+ },
1561
+ incrementalUpdate: (changedFile) => {
1562
+ try {
1563
+ const doc = parseComponent(changedFile);
1564
+ if (doc.internal) {
1565
+ removeOutputFile(outputMap, changedFile);
1566
+ if (!silent) console.log(` Skipped ${changedFile.split("/").pop()} (marked @internal)`);
1567
+ return;
1568
+ }
1569
+ const result = [{
1570
+ path: changedFile,
1571
+ doc
1572
+ }];
1573
+ let newMap;
1574
+ if (format === "json") newMap = writeJSON(result, outDir, false, silent, outputOptions);
1575
+ else newMap = writeIndividualMarkdown(result, outDir, silent, outputOptions);
1576
+ for (const [k, v] of newMap) outputMap.set(k, v);
1577
+ } catch (err) {
1578
+ const msg = err instanceof Error ? err.message : String(err);
1579
+ if (!silent) console.warn(` Warning: Could not parse ${changedFile.split("/").pop()}: ${msg}`);
1580
+ }
1581
+ },
1582
+ removeOutput: (changedFile) => {
1583
+ removeOutputFile(outputMap, changedFile);
1584
+ },
1585
+ isJoined: joined
1586
+ });
1587
+ } else if (summary && summary.errors > 0) process.exit(1);
1212
1588
  }
1213
1589
  });
1214
1590
  function collectInputPaths() {