@troshab/slidev-theme-troshab 0.1.4 → 0.1.7
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/components/Definition.vue +1 -1
- package/components/Stepper.vue +6 -0
- package/components/StyledList.vue +1 -1
- package/package.json +3 -3
- package/scripts/generate-component-docs.mjs +759 -0
- package/setup/shiki.ts +12 -3
- package/styles/base.css +155 -113
- package/styles/motion.css +1 -1
- package/fonts/IBMPlexMono-Medium.woff2 +0 -1449
- package/fonts/IBMPlexMono-Regular.woff2 +0 -1449
- package/fonts/IBMPlexSans-Bold.woff2 +0 -1449
- package/fonts/IBMPlexSans-Medium.woff2 +0 -1449
- package/fonts/IBMPlexSans-Regular.woff2 +0 -1449
- package/fonts/IBMPlexSans-SemiBold.woff2 +0 -1449
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* generate-component-docs.mjs — Sync component API docs from `.vue` source
|
|
4
|
+
*
|
|
5
|
+
* Reads `components/*.vue` entry stubs, extracts:
|
|
6
|
+
* 1. JSDoc header comment before `defineProps` (component description + prop @params)
|
|
7
|
+
* 2. `defineProps<{...}>()` / `withDefaults(defineProps<{...}>(), {...})` — props
|
|
8
|
+
* 3. `<slot name="X" />` usages in template + `useSlots()` refs — slot list
|
|
9
|
+
*
|
|
10
|
+
* Updates the matching `.md` file in the visualise-content skill (by default,
|
|
11
|
+
* `~/.claude/plugins/marketplaces/troshab-own-claude-code/plugins/troshab-own/skills/
|
|
12
|
+
* slidev-visualise-content/components/{Name}.md`) between auto-markers:
|
|
13
|
+
*
|
|
14
|
+
* <!-- BEGIN AUTO PROPS --> …generated Props table… <!-- END AUTO PROPS -->
|
|
15
|
+
* <!-- BEGIN AUTO SLOTS --> …generated Slots table… <!-- END AUTO SLOTS -->
|
|
16
|
+
* <!-- SCHEMA …JSON… -->
|
|
17
|
+
*
|
|
18
|
+
* Human-authored sections (## Usage, ## Notes, ## Examples in theme) are never
|
|
19
|
+
* touched. Missing .md → skeleton is created. Orphan .md (no matching .vue) →
|
|
20
|
+
* warning printed.
|
|
21
|
+
*
|
|
22
|
+
* Flags:
|
|
23
|
+
* --theme-dir DIR (default ~/slidev-theme-troshab)
|
|
24
|
+
* --skill-dir DIR (default visualise-content components folder)
|
|
25
|
+
* --check-only Exit 1 if any .md would change; print diff summary.
|
|
26
|
+
* --only NAMES Comma-separated component names (e.g. Stepper,Callout).
|
|
27
|
+
* Handy when debugging a single component.
|
|
28
|
+
* --no-write Print intended changes to stdout; don't touch files.
|
|
29
|
+
*
|
|
30
|
+
* Exit codes:
|
|
31
|
+
* 0 — all docs in sync (or write succeeded)
|
|
32
|
+
* 1 — drift detected with --check-only, or orphan/missing component
|
|
33
|
+
* 2 — fatal parsing error
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'node:fs'
|
|
37
|
+
import { join, resolve, dirname, basename } from 'node:path'
|
|
38
|
+
import { homedir } from 'node:os'
|
|
39
|
+
import { fileURLToPath } from 'node:url'
|
|
40
|
+
|
|
41
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
42
|
+
|
|
43
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const argv = process.argv.slice(2)
|
|
46
|
+
function flag(name) { return argv.includes(name) }
|
|
47
|
+
function opt(name, fallback) {
|
|
48
|
+
const i = argv.indexOf(name)
|
|
49
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : fallback
|
|
50
|
+
}
|
|
51
|
+
function expandTilde(p) { return p.startsWith('~') ? join(homedir(), p.slice(1)) : p }
|
|
52
|
+
|
|
53
|
+
const THEME_DIR = resolve(expandTilde(opt('--theme-dir', join(__dirname, '..'))))
|
|
54
|
+
const DEFAULT_SKILL_DIR = join(
|
|
55
|
+
homedir(),
|
|
56
|
+
'.claude', 'plugins', 'marketplaces', 'troshab-own-claude-code',
|
|
57
|
+
'plugins', 'troshab-own', 'skills', 'slidev-visualise-content', 'components',
|
|
58
|
+
)
|
|
59
|
+
const SKILL_DIR = resolve(expandTilde(opt('--skill-dir', DEFAULT_SKILL_DIR)))
|
|
60
|
+
const CHECK_ONLY = flag('--check-only')
|
|
61
|
+
const NO_WRITE = flag('--no-write')
|
|
62
|
+
const ONLY = opt('--only', null)
|
|
63
|
+
const ONLY_SET = ONLY ? new Set(ONLY.split(',').map(s => s.trim())) : null
|
|
64
|
+
|
|
65
|
+
// ─── .vue parser ──────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract the `<script setup lang="ts">` or plain `<script>` block, the template,
|
|
69
|
+
* and any top-of-file HTML comment (acts like JSDoc on entry-stub files).
|
|
70
|
+
* Returns { scriptSrc, templateSrc, headerComment } or null.
|
|
71
|
+
*/
|
|
72
|
+
function splitSfc(src) {
|
|
73
|
+
const scriptSetupMatch = src.match(/<script\s+setup(?:\s+[^>]*)?>([\s\S]*?)<\/script>/)
|
|
74
|
+
const plainScriptMatch = src.match(/<script(?:\s+[^>]*)?>([\s\S]*?)<\/script>/)
|
|
75
|
+
const scriptSrc = scriptSetupMatch ? scriptSetupMatch[1] : (plainScriptMatch ? plainScriptMatch[1] : '')
|
|
76
|
+
// Greedy + last `</template>` — avoids truncation at inner `<template v-if>`.
|
|
77
|
+
const templateMatch = src.match(/<template(?:\s[^>]*)?>([\s\S]*)<\/template>/)
|
|
78
|
+
const headerMatch = src.match(/^\s*<!--([\s\S]*?)-->/)
|
|
79
|
+
if (!scriptSrc && !headerMatch) return null
|
|
80
|
+
return {
|
|
81
|
+
scriptSrc,
|
|
82
|
+
templateSrc: templateMatch ? templateMatch[1] : '',
|
|
83
|
+
headerComment: headerMatch ? headerMatch[1] : '',
|
|
84
|
+
isSetup: !!scriptSetupMatch,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* If the entry-stub does `import X from '../components_base/X.vue'; export default X`,
|
|
90
|
+
* return the resolved path to the real implementation .vue. Otherwise null.
|
|
91
|
+
*/
|
|
92
|
+
function resolveReExport(scriptSrc, currentFile) {
|
|
93
|
+
const importMatch = scriptSrc.match(/import\s+\w+\s+from\s+['"]([^'"]+\.vue)['"]/)
|
|
94
|
+
if (!importMatch) return null
|
|
95
|
+
const exportMatch = scriptSrc.match(/export\s+default\s+\w+/)
|
|
96
|
+
if (!exportMatch) return null
|
|
97
|
+
const rel = importMatch[1]
|
|
98
|
+
return resolve(dirname(currentFile), rel)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extract the JSDoc header block right before the first `defineProps` or
|
|
103
|
+
* `withDefaults(defineProps`. Returns { description, paramDocs } where paramDocs
|
|
104
|
+
* maps propName → description string.
|
|
105
|
+
*
|
|
106
|
+
* If `fallbackHtmlComment` is provided (from an entry-stub <!-- … -->), it's
|
|
107
|
+
* parsed with the same Props:/Slots:/Usage: rules.
|
|
108
|
+
*/
|
|
109
|
+
function extractHeaderDoc(scriptSrc, fallbackHtmlComment) {
|
|
110
|
+
const defineIdx = scriptSrc.search(/(?:withDefaults\s*\(\s*)?defineProps\s*</)
|
|
111
|
+
|
|
112
|
+
let raw = ''
|
|
113
|
+
if (defineIdx >= 0) {
|
|
114
|
+
// Look backward for /** … */ ending just before defineProps.
|
|
115
|
+
const before = scriptSrc.slice(0, defineIdx)
|
|
116
|
+
const jsdocMatch = before.match(/\/\*\*[\s\S]*?\*\/(?=\s*(?:const\s+\w+\s*=\s*)?(?:withDefaults\s*\(\s*)?defineProps\s*<|\s*$)/)
|
|
117
|
+
const allMatches = [...before.matchAll(/\/\*\*[\s\S]*?\*\//g)]
|
|
118
|
+
raw = jsdocMatch ? jsdocMatch[0] : (allMatches.length ? allMatches[allMatches.length - 1][0] : '')
|
|
119
|
+
}
|
|
120
|
+
if (!raw && fallbackHtmlComment) {
|
|
121
|
+
// Treat the HTML comment as a JSDoc body (lines already look like " * foo"
|
|
122
|
+
// but may also just be plain lines — normalise accordingly).
|
|
123
|
+
raw = `/**\n${fallbackHtmlComment}\n*/`
|
|
124
|
+
}
|
|
125
|
+
if (!raw) return { description: '', paramDocs: {} }
|
|
126
|
+
|
|
127
|
+
// Clean leading " * " and trailing "*/".
|
|
128
|
+
const lines = raw
|
|
129
|
+
.replace(/^\s*\/\*\*\s*/, '')
|
|
130
|
+
.replace(/\*\/\s*$/, '')
|
|
131
|
+
.split('\n')
|
|
132
|
+
.map(l => l.replace(/^\s*\*\s?/, '').trimEnd())
|
|
133
|
+
|
|
134
|
+
const paramDocs = {}
|
|
135
|
+
const descLines = []
|
|
136
|
+
let inDescription = true
|
|
137
|
+
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
const paramMatch = line.match(/^@param\s+\{?[^}]*\}?\s*(\w+)\s*[-—]?\s*(.*)$/)
|
|
140
|
+
if (paramMatch) {
|
|
141
|
+
inDescription = false
|
|
142
|
+
paramDocs[paramMatch[1]] = paramMatch[2].trim()
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
// Some JSDocs list props inline like "value: number — Target number".
|
|
146
|
+
const inlineParamMatch = line.match(/^\s*(\w+):\s*[^—\-]+[—\-]\s+(.+)$/)
|
|
147
|
+
if (inlineParamMatch && !inDescription) {
|
|
148
|
+
paramDocs[inlineParamMatch[1]] = inlineParamMatch[2].trim()
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
// "Props:" section header switches into prop-list mode.
|
|
152
|
+
if (/^\s*Props:\s*$/i.test(line)) { inDescription = false; continue }
|
|
153
|
+
if (/^\s*Slots?:\s*$/i.test(line)) { inDescription = false; continue }
|
|
154
|
+
if (/^\s*Usage:?\s*$/i.test(line)) { inDescription = false; continue }
|
|
155
|
+
// Stop accumulating once we see a usage example, a code block, or a blank
|
|
156
|
+
// followed by an HTML-tag line — otherwise multi-line descriptions get
|
|
157
|
+
// concatenated with embedded `<Component …>` examples.
|
|
158
|
+
if (inDescription) {
|
|
159
|
+
const trimmed = line.trim()
|
|
160
|
+
if (!trimmed) { if (descLines.length) inDescription = false; continue }
|
|
161
|
+
if (/^</.test(trimmed)) { inDescription = false; continue }
|
|
162
|
+
descLines.push(trimmed)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Pass 2 for "Props:" block style ("value: number — description").
|
|
167
|
+
const propsSectionMatch = raw.match(/Props:\s*\n([\s\S]*?)(?:\n\s*(?:Slots?|Usage|Notes?|Example)s?:|\*\/)/i)
|
|
168
|
+
if (propsSectionMatch) {
|
|
169
|
+
for (const line of propsSectionMatch[1].split('\n')) {
|
|
170
|
+
const m = line.replace(/^\s*\*\s?/, '').match(/^\s*(\w+)[?!:]?\s*(?:[:\-]\s*)?[^—\-]*[—\-]\s+(.+)$/)
|
|
171
|
+
if (m && !paramDocs[m[1]]) paramDocs[m[1]] = m[2].trim()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
description: descLines.join(' ').replace(/\s+/g, ' ').trim(),
|
|
177
|
+
paramDocs,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Find defineProps<{...}>() or withDefaults(defineProps<{...}>(), {...}).
|
|
183
|
+
* Returns { propsBlock: string, defaultsBlock: string | null }.
|
|
184
|
+
*/
|
|
185
|
+
function extractPropsBlocks(scriptSrc) {
|
|
186
|
+
// Locate `defineProps<`.
|
|
187
|
+
const propsStart = scriptSrc.indexOf('defineProps<')
|
|
188
|
+
if (propsStart < 0) return null
|
|
189
|
+
|
|
190
|
+
// Balanced `<...>` extraction starting at the `<` after defineProps.
|
|
191
|
+
const openAngle = scriptSrc.indexOf('<', propsStart)
|
|
192
|
+
if (openAngle < 0) return null
|
|
193
|
+
let depth = 0
|
|
194
|
+
let typeStart = openAngle + 1
|
|
195
|
+
let typeEnd = -1
|
|
196
|
+
for (let i = openAngle; i < scriptSrc.length; i++) {
|
|
197
|
+
const c = scriptSrc[i]
|
|
198
|
+
if (c === '<') depth++
|
|
199
|
+
else if (c === '>') {
|
|
200
|
+
depth--
|
|
201
|
+
if (depth === 0) { typeEnd = i; break }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (typeEnd < 0) return null
|
|
205
|
+
const typeLiteral = scriptSrc.slice(typeStart, typeEnd).trim()
|
|
206
|
+
|
|
207
|
+
// Check if wrapped in withDefaults(..., {...}).
|
|
208
|
+
const withDefaultsIdx = scriptSrc.lastIndexOf('withDefaults', propsStart)
|
|
209
|
+
let defaultsBlock = null
|
|
210
|
+
if (withDefaultsIdx >= 0 && withDefaultsIdx < propsStart) {
|
|
211
|
+
// Locate the comma after defineProps<...>() and the { that starts defaults.
|
|
212
|
+
const afterClose = scriptSrc.indexOf(')', typeEnd)
|
|
213
|
+
if (afterClose > 0) {
|
|
214
|
+
const commaIdx = scriptSrc.indexOf(',', afterClose)
|
|
215
|
+
if (commaIdx > 0) {
|
|
216
|
+
const defBraceStart = scriptSrc.indexOf('{', commaIdx)
|
|
217
|
+
if (defBraceStart > 0) {
|
|
218
|
+
let bDepth = 0
|
|
219
|
+
let defEnd = -1
|
|
220
|
+
for (let i = defBraceStart; i < scriptSrc.length; i++) {
|
|
221
|
+
const c = scriptSrc[i]
|
|
222
|
+
if (c === '{') bDepth++
|
|
223
|
+
else if (c === '}') {
|
|
224
|
+
bDepth--
|
|
225
|
+
if (bDepth === 0) { defEnd = i; break }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (defEnd > 0) defaultsBlock = scriptSrc.slice(defBraceStart + 1, defEnd).trim()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { propsBlock: typeLiteral, defaultsBlock }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Parse `propName?: type` entries from the props-type literal. Handles union
|
|
239
|
+
* types (a|b|c), generics, nested arrays.
|
|
240
|
+
* Returns [{ name, type, required }].
|
|
241
|
+
*/
|
|
242
|
+
function parsePropsBlock(block) {
|
|
243
|
+
// Strip single-line "// …" and multi-line "/* … */" comments.
|
|
244
|
+
let stripped = block
|
|
245
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
246
|
+
.replace(/(^|\s)\/\/[^\n]*/g, (_, p) => p)
|
|
247
|
+
.trim()
|
|
248
|
+
|
|
249
|
+
// Remove outer `{...}` if present — defineProps<{ ... }>() passes the literal
|
|
250
|
+
// as `{ type?: '...'; title?: string }`; we need to walk inside the object.
|
|
251
|
+
if (stripped.startsWith('{') && stripped.endsWith('}')) {
|
|
252
|
+
stripped = stripped.slice(1, -1)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const entries = []
|
|
256
|
+
let depth = 0
|
|
257
|
+
let current = ''
|
|
258
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
259
|
+
const c = stripped[i]
|
|
260
|
+
if (c === '[' || c === '{' || c === '(') depth++
|
|
261
|
+
else if (c === ']' || c === '}' || c === ')') depth--
|
|
262
|
+
// `<` / `>` are tricky: `<T>` increases/decreases depth, but `|`/`>` in
|
|
263
|
+
// union/comparison contexts would too. For our case the prop-type literal
|
|
264
|
+
// usually stays balanced with the existing depth tracking.
|
|
265
|
+
if (c === '<' && /\w/.test(stripped[i - 1] || '')) depth++
|
|
266
|
+
else if (c === '>' && depth > 0 && stripped[i + 1] !== '=' && stripped[i - 1] !== '=') depth--
|
|
267
|
+
if ((c === ',' || c === ';' || c === '\n') && depth === 0) {
|
|
268
|
+
const piece = current.trim()
|
|
269
|
+
if (piece) entries.push(piece)
|
|
270
|
+
current = ''
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
current += c
|
|
274
|
+
}
|
|
275
|
+
if (current.trim()) entries.push(current.trim())
|
|
276
|
+
|
|
277
|
+
const props = []
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
// propName?: type
|
|
280
|
+
const m = entry.match(/^(\w+)(\??)\s*:\s*([\s\S]+)$/)
|
|
281
|
+
if (!m) continue
|
|
282
|
+
const [, name, optional, rawType] = m
|
|
283
|
+
props.push({ name, type: rawType.trim(), required: optional !== '?' })
|
|
284
|
+
}
|
|
285
|
+
return props
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Parse defaults block. Returns propName → default-value (as raw string).
|
|
290
|
+
*/
|
|
291
|
+
function parseDefaultsBlock(block) {
|
|
292
|
+
if (!block) return {}
|
|
293
|
+
const out = {}
|
|
294
|
+
let depth = 0
|
|
295
|
+
let current = ''
|
|
296
|
+
for (let i = 0; i < block.length; i++) {
|
|
297
|
+
const c = block[i]
|
|
298
|
+
if (c === '<' || c === '[' || c === '{' || c === '(') depth++
|
|
299
|
+
else if (c === '>' || c === ']' || c === '}' || c === ')') depth--
|
|
300
|
+
if ((c === ',' || c === '\n') && depth === 0) {
|
|
301
|
+
addDefault(out, current)
|
|
302
|
+
current = ''
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
current += c
|
|
306
|
+
}
|
|
307
|
+
addDefault(out, current)
|
|
308
|
+
return out
|
|
309
|
+
}
|
|
310
|
+
function addDefault(out, piece) {
|
|
311
|
+
const trimmed = piece.trim().replace(/^\/\/.*$/gm, '').trim()
|
|
312
|
+
if (!trimmed) return
|
|
313
|
+
const m = trimmed.match(/^(\w+)\s*:\s*([\s\S]+?)$/)
|
|
314
|
+
if (!m) return
|
|
315
|
+
out[m[1]] = m[2].trim()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Slots: grep static `<slot name="X" />` + dynamic `<slot :name="`step${i}`" />` +
|
|
320
|
+
* `slots.XXX` / `slots[`step${i}`]` refs. Dynamic names are emitted in their
|
|
321
|
+
* raw template form (e.g. "step${i}") so collapseSlots() can render them.
|
|
322
|
+
* Returns sorted array of unique slot names.
|
|
323
|
+
*/
|
|
324
|
+
function extractSlots(scriptSrc, templateSrc) {
|
|
325
|
+
const slots = new Set()
|
|
326
|
+
const staticSlotRe = /<slot(?:\s+name=["']([\w-]+)["'])?[\s\S]*?(?:\/>|>)/g
|
|
327
|
+
let m
|
|
328
|
+
while ((m = staticSlotRe.exec(templateSrc))) {
|
|
329
|
+
slots.add(m[1] || 'default')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Dynamic: <slot :name="`step${...}`" /> or <slot :name="'step' + idx" />.
|
|
333
|
+
const dynSlotRe = /<slot\s+:name=["']\s*`?([^`"'$]+)(?:\$\{[^}]+\}|[+\s]+[\w.]+)/g
|
|
334
|
+
while ((m = dynSlotRe.exec(templateSrc))) {
|
|
335
|
+
slots.add(`${m[1].trim()}N`)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// slots[`stepN`] / slots.stepN / $slots.stepN access patterns in script.
|
|
339
|
+
const accessRe = /(?:\$?slots)[\s]*\[[\s]*`([^`${]+)(?:\$\{[^}]+\})?`[\s]*\]/g
|
|
340
|
+
while ((m = accessRe.exec(scriptSrc))) {
|
|
341
|
+
slots.add(`${m[1].trim()}N`)
|
|
342
|
+
}
|
|
343
|
+
const dotRe = /(?:\$?slots)\.(\w+)/g
|
|
344
|
+
while ((m = dotRe.exec(scriptSrc))) slots.add(m[1])
|
|
345
|
+
|
|
346
|
+
const noise = new Set(['name', 'value', 'length', 'forEach', 'map'])
|
|
347
|
+
return [...slots].filter(s => !noise.has(s)).sort()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── Collapse long slot lists (e.g. step1..stepN) ────────────────────────────
|
|
351
|
+
|
|
352
|
+
function collapseSlots(slots) {
|
|
353
|
+
// Group patterns like step1, step2, step3 → "step1..step3".
|
|
354
|
+
const groups = new Map()
|
|
355
|
+
const singles = []
|
|
356
|
+
for (const s of slots) {
|
|
357
|
+
const m = s.match(/^([a-zA-Z]+)(\d+)$/)
|
|
358
|
+
if (!m) { singles.push(s); continue }
|
|
359
|
+
const [, prefix, num] = m
|
|
360
|
+
const arr = groups.get(prefix) || []
|
|
361
|
+
arr.push(parseInt(num, 10))
|
|
362
|
+
groups.set(prefix, arr)
|
|
363
|
+
}
|
|
364
|
+
const out = [...singles]
|
|
365
|
+
for (const [prefix, nums] of groups) {
|
|
366
|
+
nums.sort((a, b) => a - b)
|
|
367
|
+
if (nums.length === 1) out.push(`${prefix}${nums[0]}`)
|
|
368
|
+
else out.push(`${prefix}${nums[0]}..${prefix}${nums[nums.length - 1]}`)
|
|
369
|
+
}
|
|
370
|
+
return out.sort()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Markdown table renderers ────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
function renderPropsTable(props, paramDocs, defaults) {
|
|
376
|
+
if (!props.length) return '_No props._'
|
|
377
|
+
const rows = props.map(p => {
|
|
378
|
+
const def = defaults[p.name] ?? (p.required ? '—' : '—')
|
|
379
|
+
const desc = (paramDocs[p.name] || '').replace(/\|/g, '\\|')
|
|
380
|
+
return `| \`${p.name}\` | \`${p.type.replace(/\|/g, '\\|')}\` | \`${def.replace(/\|/g, '\\|')}\` | ${p.required ? 'yes' : 'no'} | ${desc} |`
|
|
381
|
+
})
|
|
382
|
+
return [
|
|
383
|
+
'| Prop | Type | Default | Required | Description |',
|
|
384
|
+
'|------|------|---------|----------|-------------|',
|
|
385
|
+
...rows,
|
|
386
|
+
].join('\n')
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Scan example_slides/*.md for usages of `<ComponentName`. Returns sorted
|
|
391
|
+
* array of { file, heading } tuples. `file` is relative to example_slides/.
|
|
392
|
+
*/
|
|
393
|
+
function findExamplesInTheme(themeDir, componentName) {
|
|
394
|
+
const dir = join(themeDir, 'example_slides')
|
|
395
|
+
if (!existsSync(dir)) return []
|
|
396
|
+
const out = []
|
|
397
|
+
for (const f of readdirSync(dir).sort()) {
|
|
398
|
+
if (!f.endsWith('.md')) continue
|
|
399
|
+
let src
|
|
400
|
+
try { src = readFileSync(join(dir, f), 'utf8') } catch { continue }
|
|
401
|
+
// Component tag: `<ComponentName` followed by whitespace, `/`, `>`, or end.
|
|
402
|
+
// Must not be inside a code fence — example_slides occasionally ship
|
|
403
|
+
// snippets showing wrong usage, but in practice the files are ~10-20 lines
|
|
404
|
+
// and any occurrence is a demo. So simple match is good enough.
|
|
405
|
+
const re = new RegExp('<' + componentName + '[\\s/>]', 'm')
|
|
406
|
+
if (!re.test(src)) continue
|
|
407
|
+
const headingMatch = src.match(/^#\s+(.+?)\s*$/m)
|
|
408
|
+
out.push({ file: f, heading: headingMatch ? headingMatch[1] : '(no heading)' })
|
|
409
|
+
}
|
|
410
|
+
return out
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function renderExamplesTable(examples) {
|
|
414
|
+
if (!examples.length) return '_No theme example slides reference this component yet._'
|
|
415
|
+
const rows = examples.map(e => `| [\`${e.file}\`](@../../../../slidev-theme-troshab/example_slides/${e.file}) | ${e.heading.replace(/\|/g, '\\|')} |`)
|
|
416
|
+
return [
|
|
417
|
+
'| File | Heading |',
|
|
418
|
+
'|------|---------|',
|
|
419
|
+
...rows,
|
|
420
|
+
].join('\n')
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function renderSlotsTable(slots, slotDescs = {}) {
|
|
424
|
+
if (!slots.length) return '_No slots._'
|
|
425
|
+
const rows = slots.map(s => {
|
|
426
|
+
const desc = (slotDescs[s] || '').replace(/\|/g, '\\|')
|
|
427
|
+
return `| \`${s}\` | ${desc} |`
|
|
428
|
+
})
|
|
429
|
+
return [
|
|
430
|
+
'| Slot | Description |',
|
|
431
|
+
'|------|-------------|',
|
|
432
|
+
...rows,
|
|
433
|
+
].join('\n')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function renderSchema(name, props, defaults, slots) {
|
|
437
|
+
const obj = {
|
|
438
|
+
name,
|
|
439
|
+
props: Object.fromEntries(props.map(p => {
|
|
440
|
+
const item = { type: p.type, required: p.required }
|
|
441
|
+
// Detect pure literal-union enum: `'a' | 'b' | 'c'` (no mixed `string` etc.).
|
|
442
|
+
const literalMatches = [...p.type.matchAll(/['"]([^'"]+)['"]/g)].map(x => x[1])
|
|
443
|
+
const residue = p.type.replace(/['"][^'"]+['"]/g, '').replace(/\s*\|\s*/g, '').trim()
|
|
444
|
+
if (literalMatches.length > 0 && residue === '') {
|
|
445
|
+
item.enum = literalMatches
|
|
446
|
+
}
|
|
447
|
+
if (defaults[p.name] !== undefined) item.default = defaults[p.name]
|
|
448
|
+
return [p.name, item]
|
|
449
|
+
})),
|
|
450
|
+
slots,
|
|
451
|
+
}
|
|
452
|
+
return JSON.stringify(obj, null, 2)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ─── .md updating ────────────────────────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
const MARKERS = {
|
|
458
|
+
propsBegin: '<!-- BEGIN AUTO PROPS -->',
|
|
459
|
+
propsEnd: '<!-- END AUTO PROPS -->',
|
|
460
|
+
slotsBegin: '<!-- BEGIN AUTO SLOTS -->',
|
|
461
|
+
slotsEnd: '<!-- END AUTO SLOTS -->',
|
|
462
|
+
examplesBegin: '<!-- BEGIN AUTO EXAMPLES -->',
|
|
463
|
+
examplesEnd: '<!-- END AUTO EXAMPLES -->',
|
|
464
|
+
schemaBegin: '<!-- SCHEMA',
|
|
465
|
+
schemaEnd: '-->',
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function replaceBetween(src, begin, end, replacement) {
|
|
469
|
+
const s = src.indexOf(begin)
|
|
470
|
+
const e = src.indexOf(end, s + begin.length)
|
|
471
|
+
if (s < 0 || e < 0) return null
|
|
472
|
+
return src.slice(0, s + begin.length) + '\n' + replacement + '\n' + src.slice(e)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function upsertSchema(src, jsonBody) {
|
|
476
|
+
const block = `${MARKERS.schemaBegin}\n${jsonBody}\n${MARKERS.schemaEnd}`
|
|
477
|
+
const idx = src.indexOf(MARKERS.schemaBegin)
|
|
478
|
+
if (idx < 0) return src.trimEnd() + '\n\n' + block + '\n'
|
|
479
|
+
// Replace from schemaBegin up to the next `-->` line.
|
|
480
|
+
const afterBegin = src.indexOf('\n', idx)
|
|
481
|
+
const endIdx = src.indexOf(MARKERS.schemaEnd, afterBegin)
|
|
482
|
+
if (endIdx < 0) return src.trimEnd() + '\n\n' + block + '\n'
|
|
483
|
+
const trail = src.slice(endIdx + MARKERS.schemaEnd.length)
|
|
484
|
+
return src.slice(0, idx) + block + trail
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function generateSkeleton(name, description, propsTable, slotsTable, examplesTable, schema) {
|
|
488
|
+
return `# ${name}
|
|
489
|
+
${description ? `\n${description}\n` : ''}
|
|
490
|
+
## Props
|
|
491
|
+
|
|
492
|
+
${MARKERS.propsBegin}
|
|
493
|
+
${propsTable}
|
|
494
|
+
${MARKERS.propsEnd}
|
|
495
|
+
|
|
496
|
+
## Slots
|
|
497
|
+
|
|
498
|
+
${MARKERS.slotsBegin}
|
|
499
|
+
${slotsTable}
|
|
500
|
+
${MARKERS.slotsEnd}
|
|
501
|
+
|
|
502
|
+
## Usage
|
|
503
|
+
|
|
504
|
+
_TODO: add representative usage example(s)._
|
|
505
|
+
|
|
506
|
+
## Notes
|
|
507
|
+
|
|
508
|
+
_TODO: add constraints, common mistakes, layout tips._
|
|
509
|
+
|
|
510
|
+
## Examples in theme
|
|
511
|
+
|
|
512
|
+
${MARKERS.examplesBegin}
|
|
513
|
+
${examplesTable}
|
|
514
|
+
${MARKERS.examplesEnd}
|
|
515
|
+
|
|
516
|
+
${MARKERS.schemaBegin}
|
|
517
|
+
${schema}
|
|
518
|
+
${MARKERS.schemaEnd}
|
|
519
|
+
`
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Pull `| propName | type | default | ... | Description |` tables out of an
|
|
524
|
+
* existing `.md` so we can preserve the hand-written Description column even
|
|
525
|
+
* when the schema otherwise gets regenerated.
|
|
526
|
+
* Returns { propName → description }. Empty if no table found.
|
|
527
|
+
*/
|
|
528
|
+
function readExistingPropDescriptions(md, heading) {
|
|
529
|
+
const headingIdx = md.search(new RegExp(`^##\\s+${heading}\\s*$`, 'm'))
|
|
530
|
+
if (headingIdx < 0) return {}
|
|
531
|
+
const lines = md.slice(headingIdx).split('\n')
|
|
532
|
+
const out = {}
|
|
533
|
+
// Find the first header row: `| Prop | ... | Description |`
|
|
534
|
+
for (let i = 0; i < lines.length; i++) {
|
|
535
|
+
const row = lines[i]
|
|
536
|
+
if (!/^\|.*Description\s*\|?$/i.test(row) && !/^\|.*Description\s*\|/i.test(row)) continue
|
|
537
|
+
// Header row found at i; data rows start at i+2 (i+1 is `|---|---|` separator).
|
|
538
|
+
for (let j = i + 2; j < lines.length; j++) {
|
|
539
|
+
const r = lines[j].trim()
|
|
540
|
+
if (!r.startsWith('|')) break
|
|
541
|
+
const cells = r.split('|').map(c => c.trim()).filter(c => c.length > 0 || true)
|
|
542
|
+
// Leading and trailing | create empty cells; strip them.
|
|
543
|
+
const inner = r.replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim())
|
|
544
|
+
if (inner.length < 2) continue
|
|
545
|
+
const propName = inner[0].replace(/^`|`$/g, '').trim()
|
|
546
|
+
const desc = inner[inner.length - 1]
|
|
547
|
+
if (propName && desc) out[propName] = desc
|
|
548
|
+
}
|
|
549
|
+
break
|
|
550
|
+
}
|
|
551
|
+
return out
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function readExistingSlotDescriptions(md) {
|
|
555
|
+
const headingIdx = md.search(/^##\s+Slots\s*$/m)
|
|
556
|
+
if (headingIdx < 0) return {}
|
|
557
|
+
const lines = md.slice(headingIdx).split('\n')
|
|
558
|
+
const out = {}
|
|
559
|
+
for (let i = 0; i < lines.length; i++) {
|
|
560
|
+
if (!/^\|\s*Slot\s*\|/i.test(lines[i])) continue
|
|
561
|
+
for (let j = i + 2; j < lines.length; j++) {
|
|
562
|
+
const r = lines[j].trim()
|
|
563
|
+
if (!r.startsWith('|')) break
|
|
564
|
+
const inner = r.replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim())
|
|
565
|
+
if (inner.length < 2) continue
|
|
566
|
+
const slotName = inner[0].replace(/^`|`$/g, '').trim()
|
|
567
|
+
const desc = inner[inner.length - 1]
|
|
568
|
+
if (slotName && desc) out[slotName] = desc
|
|
569
|
+
}
|
|
570
|
+
break
|
|
571
|
+
}
|
|
572
|
+
return out
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function updateMd(currentMd, name, description, props, paramDocs, defaults, slots, slotDescs, examples, schema) {
|
|
576
|
+
const mergedParamDocs = { ...paramDocs }
|
|
577
|
+
const mergedSlotDescs = { ...slotDescs }
|
|
578
|
+
if (currentMd) {
|
|
579
|
+
const prev = readExistingPropDescriptions(currentMd, 'Props')
|
|
580
|
+
for (const [k, v] of Object.entries(prev)) {
|
|
581
|
+
if (!mergedParamDocs[k] || mergedParamDocs[k].length < v.length) mergedParamDocs[k] = v
|
|
582
|
+
}
|
|
583
|
+
const prevSlots = readExistingSlotDescriptions(currentMd)
|
|
584
|
+
for (const [k, v] of Object.entries(prevSlots)) {
|
|
585
|
+
if (!mergedSlotDescs[k] || mergedSlotDescs[k].length < v.length) mergedSlotDescs[k] = v
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const propsTable = renderPropsTable(props, mergedParamDocs, defaults)
|
|
590
|
+
const slotsTable = renderSlotsTable(slots, mergedSlotDescs)
|
|
591
|
+
const examplesTable = renderExamplesTable(examples)
|
|
592
|
+
|
|
593
|
+
if (!currentMd) return generateSkeleton(name, description, propsTable, slotsTable, examplesTable, schema)
|
|
594
|
+
|
|
595
|
+
let out = currentMd
|
|
596
|
+
out = upsertTableBlock(out, 'Props', MARKERS.propsBegin, MARKERS.propsEnd, propsTable)
|
|
597
|
+
out = upsertTableBlock(out, 'Slots', MARKERS.slotsBegin, MARKERS.slotsEnd, slotsTable)
|
|
598
|
+
out = upsertTableBlock(out, 'Examples in theme', MARKERS.examplesBegin, MARKERS.examplesEnd, examplesTable)
|
|
599
|
+
out = upsertSchema(out, schema)
|
|
600
|
+
return out
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Replace an existing `## Heading` table (|-rows) with a managed BEGIN/END block
|
|
605
|
+
* that contains the freshly-rendered table. Leaves non-table prose inside the
|
|
606
|
+
* section untouched (e.g. Stepper's `StepperItem: { … }` note lives on).
|
|
607
|
+
*
|
|
608
|
+
* If the markers already exist — simple replace-between.
|
|
609
|
+
* If the heading exists but no markers — find the first consecutive |-row run
|
|
610
|
+
* and wrap it with markers, replacing just those lines.
|
|
611
|
+
* If the heading is missing — append a new section after the previous managed
|
|
612
|
+
* block (or at EOF).
|
|
613
|
+
*/
|
|
614
|
+
function upsertTableBlock(md, heading, beginMarker, endMarker, table) {
|
|
615
|
+
// Fast path: markers exist.
|
|
616
|
+
const replaced = replaceBetween(md, beginMarker, endMarker, table)
|
|
617
|
+
if (replaced !== null) return replaced
|
|
618
|
+
|
|
619
|
+
const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, 'm')
|
|
620
|
+
const headingMatch = md.match(headingRe)
|
|
621
|
+
if (headingMatch) {
|
|
622
|
+
const headingIdx = md.indexOf(headingMatch[0])
|
|
623
|
+
const afterHeading = md.indexOf('\n', headingIdx) + 1
|
|
624
|
+
const rest = md.slice(afterHeading)
|
|
625
|
+
const lines = rest.split('\n')
|
|
626
|
+
// Find start of the table (first line starting with `|`).
|
|
627
|
+
let tableStart = -1
|
|
628
|
+
let tableEnd = -1
|
|
629
|
+
for (let i = 0; i < lines.length; i++) {
|
|
630
|
+
const trimmed = lines[i].trim()
|
|
631
|
+
if (trimmed.startsWith('##')) break
|
|
632
|
+
if (tableStart < 0 && trimmed.startsWith('|')) tableStart = i
|
|
633
|
+
if (tableStart >= 0 && !trimmed.startsWith('|') && tableEnd < 0) {
|
|
634
|
+
tableEnd = i
|
|
635
|
+
break
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (tableStart >= 0) {
|
|
639
|
+
if (tableEnd < 0) tableEnd = lines.length
|
|
640
|
+
const before = lines.slice(0, tableStart).join('\n')
|
|
641
|
+
const after = lines.slice(tableEnd).join('\n')
|
|
642
|
+
const block = `${beginMarker}\n${table}\n${endMarker}`
|
|
643
|
+
const rebuilt = [before.trimEnd(), block, after.trimStart()].filter(Boolean).join('\n\n')
|
|
644
|
+
return md.slice(0, afterHeading) + rebuilt
|
|
645
|
+
}
|
|
646
|
+
// Heading exists but no table — inject block right after heading line.
|
|
647
|
+
const block = `\n${beginMarker}\n${table}\n${endMarker}\n`
|
|
648
|
+
return md.slice(0, afterHeading) + block + rest
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// No heading — append at EOF (before any schema comment) as a new section.
|
|
652
|
+
const schemaIdx = md.indexOf(MARKERS.schemaBegin)
|
|
653
|
+
const newSection = `\n## ${heading}\n\n${beginMarker}\n${table}\n${endMarker}\n`
|
|
654
|
+
if (schemaIdx < 0) return md.trimEnd() + '\n' + newSection
|
|
655
|
+
return md.slice(0, schemaIdx).trimEnd() + '\n' + newSection + '\n' + md.slice(schemaIdx)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ─── Orchestrate ─────────────────────────────────────────────────────────────
|
|
659
|
+
|
|
660
|
+
function listVueComponents() {
|
|
661
|
+
const dir = join(THEME_DIR, 'components')
|
|
662
|
+
if (!existsSync(dir)) { console.error(`[gen-docs] missing ${dir}`); process.exit(2) }
|
|
663
|
+
return readdirSync(dir).filter(f => f.endsWith('.vue')).map(f => ({
|
|
664
|
+
name: basename(f, '.vue'),
|
|
665
|
+
path: join(dir, f),
|
|
666
|
+
}))
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function listMdDocs() {
|
|
670
|
+
if (!existsSync(SKILL_DIR)) { mkdirSync(SKILL_DIR, { recursive: true }) }
|
|
671
|
+
return readdirSync(SKILL_DIR).filter(f => f.endsWith('.md'))
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function diffSummary(oldSrc, newSrc) {
|
|
675
|
+
if (oldSrc === newSrc) return null
|
|
676
|
+
const oldLines = (oldSrc || '').split('\n').length
|
|
677
|
+
const newLines = newSrc.split('\n').length
|
|
678
|
+
return `${oldLines} → ${newLines} lines`
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function main() {
|
|
682
|
+
const components = listVueComponents()
|
|
683
|
+
const existingMds = new Set(listMdDocs())
|
|
684
|
+
let changed = 0
|
|
685
|
+
let errors = 0
|
|
686
|
+
const orphans = []
|
|
687
|
+
|
|
688
|
+
for (const { name, path } of components) {
|
|
689
|
+
if (ONLY_SET && !ONLY_SET.has(name)) continue
|
|
690
|
+
const vueSrc = readFileSync(path, 'utf8')
|
|
691
|
+
const parts = splitSfc(vueSrc)
|
|
692
|
+
if (!parts) {
|
|
693
|
+
console.error(`[gen-docs] ${name}: could not parse SFC`)
|
|
694
|
+
errors++
|
|
695
|
+
continue
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Parse props from this file; if missing and it's a re-export, follow the
|
|
699
|
+
// import to the real implementation (components_base/X.vue).
|
|
700
|
+
let propsBlocks = extractPropsBlocks(parts.scriptSrc)
|
|
701
|
+
let slotsScriptSrc = parts.scriptSrc
|
|
702
|
+
let slotsTemplateSrc = parts.templateSrc
|
|
703
|
+
if (!propsBlocks) {
|
|
704
|
+
const reExport = resolveReExport(parts.scriptSrc, path)
|
|
705
|
+
if (reExport && existsSync(reExport)) {
|
|
706
|
+
const baseSrc = readFileSync(reExport, 'utf8')
|
|
707
|
+
const baseParts = splitSfc(baseSrc)
|
|
708
|
+
if (baseParts) {
|
|
709
|
+
propsBlocks = extractPropsBlocks(baseParts.scriptSrc)
|
|
710
|
+
slotsScriptSrc = baseParts.scriptSrc
|
|
711
|
+
slotsTemplateSrc = baseParts.templateSrc
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const { description, paramDocs } = extractHeaderDoc(parts.scriptSrc, parts.headerComment)
|
|
717
|
+
const props = propsBlocks ? parsePropsBlock(propsBlocks.propsBlock) : []
|
|
718
|
+
const defaults = propsBlocks ? parseDefaultsBlock(propsBlocks.defaultsBlock) : {}
|
|
719
|
+
const slotsRaw = extractSlots(slotsScriptSrc, slotsTemplateSrc)
|
|
720
|
+
const slots = collapseSlots(slotsRaw)
|
|
721
|
+
const examples = findExamplesInTheme(THEME_DIR, name)
|
|
722
|
+
|
|
723
|
+
const schema = renderSchema(name, props, defaults, slots)
|
|
724
|
+
|
|
725
|
+
const mdPath = join(SKILL_DIR, `${name}.md`)
|
|
726
|
+
const existingMd = existsSync(mdPath) ? readFileSync(mdPath, 'utf8') : ''
|
|
727
|
+
existingMds.delete(`${name}.md`)
|
|
728
|
+
|
|
729
|
+
const newMd = updateMd(existingMd, name, description, props, paramDocs, defaults, slots, {}, examples, schema)
|
|
730
|
+
const diff = diffSummary(existingMd, newMd)
|
|
731
|
+
if (diff) {
|
|
732
|
+
changed++
|
|
733
|
+
if (CHECK_ONLY) console.log(`[drift] ${name}.md — ${diff}`)
|
|
734
|
+
else if (NO_WRITE) console.log(`[would-write] ${name}.md — ${diff}`)
|
|
735
|
+
else { writeFileSync(mdPath, newMd, 'utf8'); console.log(`[write] ${name}.md — ${diff}`) }
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Orphans — .md files in skill-dir with no matching .vue.
|
|
740
|
+
for (const md of existingMds) {
|
|
741
|
+
if (ONLY_SET) continue
|
|
742
|
+
orphans.push(md)
|
|
743
|
+
}
|
|
744
|
+
if (orphans.length) {
|
|
745
|
+
console.log(`[orphan] ${orphans.length} .md file(s) without matching .vue in theme:`)
|
|
746
|
+
for (const o of orphans) console.log(` ${o}`)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (CHECK_ONLY) {
|
|
750
|
+
if (changed > 0 || errors > 0 || orphans.length > 0) process.exit(1)
|
|
751
|
+
console.log('[gen-docs] All docs in sync')
|
|
752
|
+
process.exit(0)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
console.log(`[gen-docs] ${components.length} components, ${changed} updated, ${errors} error(s), ${orphans.length} orphan(s)`)
|
|
756
|
+
process.exit(errors > 0 ? 2 : 0)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
main()
|