compmark-vue
v0.4.0
Published
Auto-generate Markdown documentation from Vue 3 SFCs
Maintainers
Readme
compmark-vue
Auto-generate Markdown documentation from Vue 3 SFCs. Zero configuration required.
Quick Start
Document a single component:
npx compmark-vue ./src/components/Button.vueDocument an entire directory:
npx compmark-vue ./src/components --out ./docs/apiInstallation
# npm
npm install -D compmark-vue
# pnpm
pnpm add -D compmark-vue
# yarn
yarn add -D compmark-vueRequires Node.js >= 20
Add to your package.json post installation
{
"scripts": {
"docs": "compmark ./src/components --out ./docs/api"
}
}CLI
compmark <files/dirs/globs> [options]| Option | Description | Default |
| ---------------------- | ---------------------------------- | ------- |
| --out <dir> | Output directory | . |
| --format <md\|json> | Output format | md |
| --join | Combine into a single file | |
| --ignore <patterns> | Comma-separated ignore patterns | |
| --preserve-structure | Mirror input folder tree in output | |
| --watch | Watch for changes and rebuild | |
| --silent | Suppress non-error output | |
Examples
# Single file
compmark Button.vue
# Directory (recursive)
compmark src/components --out docs/api
# Glob pattern
compmark "src/**/components/*.vue" --out docs
# Monorepo
compmark "packages/*/src/components" --out docs/api
# Combined markdown with table of contents
compmark src/components --out docs --join
# JSON output
compmark src/components --out docs/api --format json
# JSON combined into single file
compmark src/components --format json --join --out docs
# Ignore patterns
compmark src/components --ignore "internal,*.test"
# Preserve folder structure in output
compmark src --out docs --preserve-structure
# src/components/Button.vue → docs/components/Button.md
# src/views/Home.vue → docs/views/Home.md
# Watch mode
compmark src/components --out docs --watch
# Multiple inputs
compmark src/components src/layouts --out docsThe summary line shows what happened:
✓ 24 components documented, 2 skipped, 0 errorsSkipped count includes both @internal components and files matching --ignore patterns.
Exit code is 1 when errors occur (except in watch mode).
In watch mode, individual file changes are processed incrementally — only the changed file is re-parsed and re-written. If a file is deleted or becomes @internal, its output is automatically removed.
Features
- Component description — JSDoc on the first statement becomes a component summary
- Refs —
ref(),shallowRef(),reactive(),shallowReactive()with type inference - Computed —
computed()with generic and return-type inference - Props — runtime and TypeScript generic syntax, including imported types
- Emits — array, TypeScript property, and call signature syntax
- Slots —
defineSlotswith typed bindings, template<slot>fallback - Expose —
defineExposewith JSDoc descriptions - Composables — auto-detects
useX()calls in<script setup> - JSDoc tags —
@deprecated,@since,@example,@see,@default @internal— exclude components from output- Options API —
export default { props, emits, data(), computed }support - Output formats — Markdown (individual or joined), JSON
- Preserve structure — mirror input folder tree in
--out - Empty sections are skipped cleanly — no placeholder noise
Examples
Component Description
A JSDoc comment at the top of <script setup> is extracted as the component description and included in the generated .md file:
<!-- ConfirmDialog.vue -->
<template>
<dialog :open="open">
<slot />
<button @click="$emit('confirm')">OK</button>
<button @click="$emit('cancel')">Cancel</button>
</dialog>
</template>
<script setup lang="ts">
/**
* A dialog for confirming destructive user actions such as deletions.
*/
import { ref } from "vue";
const props = defineProps<{
/** Whether the dialog is visible */
open: boolean;
/** Dialog title */
title?: string;
}>();
const emit = defineEmits<{
/** Emitted when the user confirms the action */
confirm: [];
/** Emitted when the user cancels */
cancel: [];
}>();
const loading = ref(false);
</script>Running compmark ConfirmDialog.vue produces ConfirmDialog.md:
# ConfirmDialog
A dialog for confirming destructive user actions such as deletions.
**Note:** Uses `<script setup>` syntax.
## Refs
| Name | Type | Description |
| ------- | ------------------ | ----------- |
| loading | Ref<boolean> | - |
## Props
| Name | Type | Required | Default | Description |
| ----- | ------- | -------- | ------- | ----------------------------- |
| open | boolean | Yes | - | Whether the dialog is visible |
| title | string | No | - | Dialog title |
## Emits
| Name | Description |
| ------- | ----------------------------------------- |
| confirm | Emitted when the user confirms the action |
| cancel | Emitted when the user cancels |
## Slots
| Name | Bindings | Description |
| ------- | -------- | ----------- |
| default | - | - |The description is picked up automatically when the JSDoc comment is on:
- An
importstatement (most common — put the comment at the top of the script) - A
defineProps/defineEmitscall (bare orconst props = defineProps(...)) - A
withDefaults(...)call
If the first statement is a plain variable (like const count = ref(0)), use the @component tag so the comment isn't mistaken for a variable description:
<script setup lang="ts">
/**
* @component
* A tooltip that appears on hover.
*/
const visible = ref(false);
</script>Refs
ref(), shallowRef(), reactive(), and shallowReactive() in <script setup> are automatically documented with inferred types:
<script setup lang="ts">
import { ref, shallowRef, reactive, computed } from "vue";
/**
* The counter value
* @since 1.0.0
*/
const count = ref(0);
/** The user's name */
const name = ref<string>("");
/** @deprecated Use shallowData instead */
const data = shallowRef<string[]>([]);
const state = reactive({ count: 0, name: "" });
</script>Output:
## Refs
| Name | Type | Description |
| ----- | -------------------------- | ----------------------------------------- |
| count | Ref<number> | The counter value _(since 1.0.0)_ |
| name | Ref<string> | The user's name |
| data | ShallowRef<string[]> | - **Deprecated**: Use shallowData instead |
| state | Object | - |Type inference priority:
- Explicit TS annotation:
const x: Ref<string[]> = ref([])→Ref<string[]> - Generic type parameter:
ref<string>("")→Ref<string> - Literal argument:
ref(0)→Ref<number>,ref("")→Ref<string>,ref(true)→Ref<boolean> - Fallback:
Ref(no type parameter)
Computed
computed() calls are documented with type inference from generics or getter return types:
<script setup lang="ts">
import { ref, computed } from "vue";
const count = ref(0);
/**
* The full display name
* @since 2.0.0
*/
const fullName = computed(() => "John Smith");
const total = computed<number>(() => count.value * 2);
const doubled = computed((): number => count.value * 2);
</script>Output:
## Computed
| Name | Type | Description |
| -------- | ------------------------- | ------------------------------------- |
| fullName | ComputedRef | The full display name _(since 2.0.0)_ |
| total | ComputedRef<number> | - |
| doubled | ComputedRef<number> | - |Props
Runtime syntax, TypeScript generics, and withDefaults are all supported:
<script setup lang="ts">
const props = withDefaults(
defineProps<{
/** The label text */
label: string;
/** Visual theme */
theme?: "filled" | "outline";
disabled?: boolean;
}>(),
{
theme: "filled",
disabled: false,
},
);
</script>Output:
## Props
| Name | Type | Required | Default | Description |
| -------- | --------------------- | -------- | ---------- | -------------- |
| label | string | Yes | - | The label text |
| theme | 'filled' \| 'outline' | No | `"filled"` | Visual theme |
| disabled | boolean | No | `false` | - |Runtime object syntax is also supported:
<script setup>
defineProps({
/** Title of the dialog */
title: {
type: String,
required: true,
},
visible: {
type: Boolean,
default: false,
},
});
</script>Imported types
defineProps<ImportedType>() with exported interfaces or type aliases is supported:
// types.ts
export interface ButtonProps {
/** The label text */
label: string;
disabled?: boolean;
}<script setup lang="ts">
import type { ButtonProps } from "./types";
defineProps<ButtonProps>();
</script>Interface extends is resolved (up to 5 levels deep). withDefaults works with imported types too.
Emits
TypeScript generic syntax with payloads:
<script setup lang="ts">
const emit = defineEmits<{
/** Emitted on save */
save: [data: Record<string, unknown>];
/** Emitted on cancel */
cancel: [];
}>();
</script>Output:
## Emits
| Name | Payload | Description |
| ------ | ----------------------------- | ----------------- |
| save | data: Record<string, unknown> | Emitted on save |
| cancel | - | Emitted on cancel |Call signature syntax is also supported:
<script setup lang="ts">
defineEmits<{
(e: "click", payload: MouseEvent): void;
(e: "submit"): void;
}>();
</script>Array syntax works too: defineEmits(["click", "submit"]).
Slots
defineSlots provides typed bindings:
<script setup lang="ts">
defineSlots<{
/** Main content */
default(props: { msg: string }): any;
/** Header area */
header(props: { title: string; count: number }): any;
}>();
</script>Output:
## Slots
| Name | Bindings | Description |
| ------- | ---------------------------- | ------------ |
| default | msg: string | Main content |
| header | title: string, count: number | Header area |If defineSlots is not used, slots are extracted from template <slot> elements as a fallback:
<template>
<div>
<slot />
<slot name="header" :title="title" />
<slot name="footer" />
</div>
</template>Expose
<script setup lang="ts">
defineExpose({
/** Focus the component */
focus,
/** Reset the component state */
reset,
});
</script>Output:
## Exposed
| Name | Type | Description |
| ----- | ------- | ------------------------- |
| focus | unknown | Focus the component |
| reset | unknown | Reset the component state |Composables
Any useX() calls in <script setup> are automatically detected. Variable bindings (simple assignment, object/array destructuring, rest elements) are extracted:
<script setup lang="ts">
import { useRouter } from "vue-router";
import { useMouse } from "@vueuse/core";
import { useAuth } from "./composables/useAuth";
const router = useRouter();
const { x, y } = useMouse();
const { user, login, logout } = useAuth();
useHead({ title: "My App" });
</script>Output:
## Composables Used
### `useRouter`
**Returns:** `router`
### `useMouse`
**Returns:** `x`, `y`
### `useAuth`
_Source: `./composables/useAuth`_
| Variable | Type |
| -------- | ------------------------------------------- |
| user | Ref<User> |
| login | (credentials: Credentials) => Promise<void> |
| logout | () => void |
### `useHead`
Called for side effects.For local imports (./ or @/ paths), types are automatically resolved from the composable source file — ref(), computed(), reactive(), function signatures, and literals are all inferred. Source attribution is shown for local imports only.
JSDoc Tags
Props support @deprecated, @since, @example, and @see:
<script setup lang="ts">
defineProps<{
/**
* The label text
* @deprecated Use `text` instead
* @since 1.0.0
* @example "Hello World"
* @see https://example.com/docs
*/
label: string;
}>();
</script>Output:
## Props
| Name | Type | Required | Default | Description |
| ----- | ------ | -------- | ------- | ----------------------------------------------------------------------------------------------- |
| label | string | Yes | - | The label text **Deprecated**: Use `text` instead _(since 1.0.0)_ See: https://example.com/docs |
**`label` example:**
```
"Hello World"
```Internal Components
Mark a component with @internal to skip it during generation:
<script setup lang="ts">
/**
* @internal
*/
defineProps<{
value: string;
}>();
</script>$ compmark InternalHelper.vue
Skipped InternalHelper.vue (marked @internal)
✓ 0 components documented, 1 skipped, 0 errorsOptions API
Components using export default {} are supported. Props, emits, data(), and computed are all extracted:
<script>
export default {
props: {
/** The title text */
title: {
type: String,
required: true,
},
count: {
type: Number,
default: 10,
},
},
emits: ["click", "update"],
data() {
return {
/** The greeting message */
message: "Hello",
isActive: true,
};
},
computed: {
/**
* The full display name
* @since 1.0.0
*/
fullName() {
return this.title + " Smith";
},
},
};
</script>Output:
## Refs
| Name | Type | Description |
| -------- | ------- | -------------------- |
| message | string | The greeting message |
| isActive | boolean | - |
## Computed
| Name | Type | Description |
| -------- | ------- | ------------------------------------- |
| fullName | unknown | The full display name _(since 1.0.0)_ |
## Props
| Name | Type | Required | Default | Description |
| ----- | ------ | -------- | ------- | -------------- |
| title | String | Yes | - | The title text |
| count | Number | No | `10` | - |
## Emits
| Name | Description |
| ------ | ----------- |
| click | - |
| update | - |data() return properties are documented as refs with literal type inference. Computed properties support both simple getters and get/set object syntax.
Output Formats
Individual markdown (default) — one .md file per component:
compmark src/components --out docs
# Creates: docs/Button.md, docs/Dialog.md, ...Joined markdown — single file with table of contents:
compmark src/components --out docs --join
# Creates: docs/components.mdThe joined file includes a generated timestamp, table of contents with anchor links, and all components with headings bumped one level.
JSON — machine-readable output:
# Individual JSON files
compmark src/components --out docs --format json
# Creates: docs/Button.json, docs/Dialog.json, ...
# Combined JSON
compmark src/components --format json --join --out docs
# Creates: docs/components.json with { generated, components: [...] }Preserve Structure
By default, all output files are placed flat in the --out directory. Use --preserve-structure to mirror the input folder hierarchy:
compmark src --out docs/api --preserve-structuresrc/components/Button.vue → docs/api/components/Button.md
src/components/Dialog.vue → docs/api/components/Dialog.md
src/views/Home.vue → docs/api/views/Home.mdWithout --preserve-structure, components with the same name in different directories get a numeric suffix (Button.md, Button-2.md). With --preserve-structure, they live in separate subdirectories.
--join mode ignores --preserve-structure (single output file).
Programmatic API
pnpm install compmark-vueimport { parseComponent, generateMarkdown } from "compmark-vue";
const doc = parseComponent("./src/components/Button.vue");
const md = generateMarkdown(doc);Or parse from a string:
import { parseSFC, generateMarkdown } from "compmark-vue";
const doc = parseSFC(source, "Button.vue");
const md = generateMarkdown(doc);Multi-file processing:
import { discoverFiles, processFiles } from "compmark-vue";
const { files, ignoredCount, basePath } = await discoverFiles(["src/components"], ["dist"]);
const summary = processFiles(files, { silent: false });
// summary.files, summary.documented, summary.skipped, summary.errorsDevelopment
- Clone this repository
- Install latest LTS version of Node.js (>= 20)
- Enable Corepack using
corepack enable - Install dependencies using
pnpm install - Run interactive tests using
pnpm dev
License
Published under the MIT license.
