@bfra.me/doc-sync 0.1.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.
Files changed (50) hide show
  1. package/README.md +288 -0
  2. package/lib/chunk-6NKAJT2M.js +1233 -0
  3. package/lib/chunk-DR6UG237.js +1027 -0
  4. package/lib/chunk-G5KKGJYO.js +1560 -0
  5. package/lib/chunk-ROLA7SBB.js +12 -0
  6. package/lib/cli/index.d.ts +1 -0
  7. package/lib/cli/index.js +397 -0
  8. package/lib/generators/index.d.ts +170 -0
  9. package/lib/generators/index.js +76 -0
  10. package/lib/index.d.ts +141 -0
  11. package/lib/index.js +118 -0
  12. package/lib/parsers/index.d.ts +264 -0
  13. package/lib/parsers/index.js +113 -0
  14. package/lib/types.d.ts +388 -0
  15. package/lib/types.js +7 -0
  16. package/package.json +99 -0
  17. package/src/cli/commands/index.ts +3 -0
  18. package/src/cli/commands/sync.ts +146 -0
  19. package/src/cli/commands/validate.ts +151 -0
  20. package/src/cli/commands/watch.ts +74 -0
  21. package/src/cli/index.ts +71 -0
  22. package/src/cli/types.ts +19 -0
  23. package/src/cli/ui.ts +123 -0
  24. package/src/generators/api-reference-generator.ts +268 -0
  25. package/src/generators/code-example-formatter.ts +313 -0
  26. package/src/generators/component-mapper.ts +383 -0
  27. package/src/generators/content-merger.ts +295 -0
  28. package/src/generators/frontmatter-generator.ts +277 -0
  29. package/src/generators/index.ts +56 -0
  30. package/src/generators/mdx-generator.ts +289 -0
  31. package/src/index.ts +131 -0
  32. package/src/orchestrator/index.ts +21 -0
  33. package/src/orchestrator/package-scanner.ts +276 -0
  34. package/src/orchestrator/sync-orchestrator.ts +382 -0
  35. package/src/orchestrator/validation-pipeline.ts +328 -0
  36. package/src/parsers/export-analyzer.ts +335 -0
  37. package/src/parsers/guards.ts +350 -0
  38. package/src/parsers/index.ts +82 -0
  39. package/src/parsers/jsdoc-extractor.ts +313 -0
  40. package/src/parsers/package-info.ts +267 -0
  41. package/src/parsers/readme-parser.ts +334 -0
  42. package/src/parsers/typescript-parser.ts +299 -0
  43. package/src/types.ts +423 -0
  44. package/src/utils/index.ts +13 -0
  45. package/src/utils/safe-patterns.ts +280 -0
  46. package/src/utils/sanitization.ts +164 -0
  47. package/src/watcher/change-detector.ts +138 -0
  48. package/src/watcher/debouncer.ts +168 -0
  49. package/src/watcher/file-watcher.ts +164 -0
  50. package/src/watcher/index.ts +27 -0
@@ -0,0 +1,151 @@
1
+ import type {GlobalOptions, ValidationStatus} from '../types.js'
2
+
3
+ import fs from 'node:fs/promises'
4
+ import path from 'node:path'
5
+ import process from 'node:process'
6
+
7
+ import {generateMDXDocument, mergeContent} from '../../generators/index.js'
8
+ import {createPackageScanner} from '../../orchestrator/package-scanner.js'
9
+ import {createValidationPipeline} from '../../orchestrator/validation-pipeline.js'
10
+ import {getUnscopedName} from '../../parsers/index.js'
11
+ import {createLogger, createSpinner, formatDuration, showIntro, showOutro} from '../ui.js'
12
+
13
+ export async function runValidate(packages: string[], options: GlobalOptions): Promise<void> {
14
+ const logger = createLogger(options)
15
+ const rootDir = path.resolve(options.root)
16
+ const outputDir = path.join(rootDir, 'docs', 'src', 'content', 'docs', 'packages')
17
+
18
+ if (options.quiet !== true) {
19
+ showIntro('🔍 doc-sync validate')
20
+ }
21
+
22
+ const startTime = Date.now()
23
+ const spinner = options.quiet === true ? undefined : createSpinner()
24
+ const validationResults: ValidationStatus[] = []
25
+
26
+ spinner?.start('Scanning packages...')
27
+
28
+ const scanner = createPackageScanner({
29
+ rootDir,
30
+ parseSourceFiles: true,
31
+ parseReadme: true,
32
+ })
33
+
34
+ const validationPipeline = createValidationPipeline()
35
+ const scanResult = await scanner.scan()
36
+
37
+ spinner?.stop(`Found ${scanResult.packages.length} packages`)
38
+
39
+ const packagesToValidate =
40
+ packages.length > 0
41
+ ? scanResult.packages.filter(pkg => packages.includes(pkg.info.name))
42
+ : scanResult.packages
43
+
44
+ spinner?.start(`Validating ${packagesToValidate.length} packages...`)
45
+
46
+ for (const pkg of packagesToValidate) {
47
+ const issues: string[] = []
48
+ let isValid = true
49
+
50
+ const docPath = buildDocPath(pkg.info.name, outputDir)
51
+
52
+ try {
53
+ await fs.access(docPath)
54
+
55
+ const existingContent = await fs.readFile(docPath, 'utf-8')
56
+ const contentValidation = validationPipeline.validateContent(existingContent)
57
+
58
+ if (!contentValidation.valid) {
59
+ isValid = false
60
+ for (const error of contentValidation.errors) {
61
+ issues.push(`MDX error: ${error.message}`)
62
+ }
63
+ }
64
+
65
+ const docResult = generateMDXDocument(pkg.info, pkg.readme, pkg.api)
66
+
67
+ if (docResult.success) {
68
+ const generatedContent = docResult.data.rendered
69
+ const mergedResult = mergeContent(existingContent, generatedContent)
70
+
71
+ // Use mergeContent to respect manual edits between sentinel markers
72
+ // This ensures validation matches sync behavior
73
+ if (mergedResult.success) {
74
+ if (normalizeContent(existingContent) !== normalizeContent(mergedResult.data.content)) {
75
+ isValid = false
76
+ issues.push('Documentation is out of date with source')
77
+ }
78
+ } else if (normalizeContent(existingContent) !== normalizeContent(generatedContent)) {
79
+ isValid = false
80
+ issues.push('Documentation is out of date with source')
81
+ }
82
+ } else {
83
+ issues.push(`Generation failed: ${docResult.error.message}`)
84
+ }
85
+ } catch {
86
+ isValid = false
87
+ issues.push('Documentation file does not exist')
88
+ }
89
+
90
+ validationResults.push({
91
+ packageName: pkg.info.name,
92
+ isValid,
93
+ issues,
94
+ })
95
+
96
+ if (options.verbose === true) {
97
+ if (isValid) {
98
+ logger.success(`✓ ${pkg.info.name}`)
99
+ } else {
100
+ logger.warn(`✗ ${pkg.info.name}: ${issues.join(', ')}`)
101
+ }
102
+ }
103
+ }
104
+
105
+ spinner?.stop('Validation complete')
106
+
107
+ const validCount = validationResults.filter(r => r.isValid).length
108
+ const invalidCount = validationResults.filter(r => !r.isValid).length
109
+ const durationMs = Date.now() - startTime
110
+
111
+ if (invalidCount > 0) {
112
+ logger.warn(`Found ${invalidCount} package${invalidCount === 1 ? '' : 's'} with issues:`)
113
+
114
+ for (const result of validationResults) {
115
+ if (!result.isValid) {
116
+ logger.error(` ${result.packageName}:`)
117
+ for (const issue of result.issues) {
118
+ logger.error(` - ${issue}`)
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ logger.info(`${validCount} valid, ${invalidCount} invalid in ${formatDuration(durationMs)}`)
125
+
126
+ if (options.quiet !== true) {
127
+ if (invalidCount > 0) {
128
+ showOutro(
129
+ `⚠️ ${invalidCount} package${invalidCount === 1 ? '' : 's'} need${invalidCount === 1 ? 's' : ''} attention`,
130
+ )
131
+ } else {
132
+ showOutro('✨ All documentation is up to date!')
133
+ }
134
+ }
135
+
136
+ if (invalidCount > 0) {
137
+ process.exit(1)
138
+ }
139
+ }
140
+
141
+ function buildDocPath(packageName: string, outputDir: string): string {
142
+ const slug = getUnscopedName(packageName)
143
+ .toLowerCase()
144
+ .replaceAll(/[^a-z0-9-]/g, '-')
145
+
146
+ return path.join(outputDir, `${slug}.mdx`)
147
+ }
148
+
149
+ function normalizeContent(content: string): string {
150
+ return content.replaceAll('\r\n', '\n').replaceAll(/\s+$/gm, '').trim()
151
+ }
@@ -0,0 +1,74 @@
1
+ import type {GlobalOptions} from '../types.js'
2
+
3
+ import path from 'node:path'
4
+ import process from 'node:process'
5
+
6
+ import {createSyncOrchestrator} from '../../orchestrator/sync-orchestrator.js'
7
+ import {createLogger, createProgressCallback, createSpinner, showIntro, showOutro} from '../ui.js'
8
+
9
+ export async function runWatch(packages: string[], options: GlobalOptions): Promise<void> {
10
+ const logger = createLogger(options)
11
+ const rootDir = path.resolve(options.root)
12
+ const outputDir = path.join(rootDir, 'docs', 'src', 'content', 'docs', 'packages')
13
+
14
+ if (options.quiet !== true) {
15
+ showIntro('👁️ doc-sync watch')
16
+ }
17
+
18
+ const spinner = options.quiet === true ? undefined : createSpinner()
19
+
20
+ const orchestrator = createSyncOrchestrator({
21
+ config: {
22
+ rootDir,
23
+ outputDir,
24
+ includePatterns: ['packages/*'],
25
+ excludePatterns: [],
26
+ watch: true,
27
+ debounceMs: 300,
28
+ },
29
+ dryRun: options.dryRun ?? false,
30
+ verbose: options.verbose ?? false,
31
+ onProgress: createProgressCallback(options),
32
+ onError(error): void {
33
+ logger.error(`Error: ${error.message}`)
34
+ },
35
+ })
36
+
37
+ if (packages.length > 0) {
38
+ logger.info(`Watching packages: ${packages.join(', ')}`)
39
+ spinner?.start('Performing initial sync...')
40
+ await orchestrator.syncPackages(packages)
41
+ spinner?.stop('Initial sync complete')
42
+ } else {
43
+ spinner?.start('Performing initial sync of all packages...')
44
+ await orchestrator.syncAll()
45
+ spinner?.stop('Initial sync complete')
46
+ }
47
+
48
+ logger.info('Starting watch mode...')
49
+ spinner?.start('Watching for changes... (Ctrl+C to stop)')
50
+
51
+ await orchestrator.startWatching()
52
+
53
+ const shutdown = async (): Promise<void> => {
54
+ spinner?.stop('Stopping watch mode...')
55
+ await orchestrator.stopWatching()
56
+
57
+ if (options.quiet !== true) {
58
+ showOutro('👋 Watch mode stopped')
59
+ }
60
+
61
+ process.exit(0)
62
+ }
63
+
64
+ process.on('SIGINT', () => {
65
+ shutdown().catch(() => process.exit(1))
66
+ })
67
+
68
+ process.on('SIGTERM', () => {
69
+ shutdown().catch(() => process.exit(1))
70
+ })
71
+
72
+ // Keep the process running until interrupted
73
+ setInterval(() => {}, 1 << 30)
74
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @bfra.me/doc-sync CLI entry point
4
+ *
5
+ * Modern TUI for documentation synchronization using @clack/prompts.
6
+ */
7
+
8
+ import type {GlobalOptions} from './types.js'
9
+
10
+ import process from 'node:process'
11
+
12
+ import {cac} from 'cac'
13
+ import {runSync} from './commands/sync.js'
14
+ import {runValidate} from './commands/validate.js'
15
+ import {runWatch} from './commands/watch.js'
16
+
17
+ const cli = cac('doc-sync')
18
+
19
+ cli
20
+ .command(
21
+ 'sync [...packages]',
22
+ 'Sync documentation for specified packages (or all if none specified)',
23
+ )
24
+ .option('-r, --root <dir>', 'Root directory of the monorepo', {default: process.cwd()})
25
+ .option('-d, --dry-run', 'Preview changes without writing files')
26
+ .option('-v, --verbose', 'Enable verbose output')
27
+ .option('-q, --quiet', 'Suppress non-error output')
28
+ .option('-i, --interactive', 'Use interactive package selection')
29
+ .action(async (packages: string[], options: GlobalOptions) => {
30
+ await runSync(packages, options)
31
+ })
32
+
33
+ cli
34
+ .command('watch [...packages]', 'Watch for changes and sync automatically')
35
+ .option('-r, --root <dir>', 'Root directory of the monorepo', {default: process.cwd()})
36
+ .option('-d, --dry-run', 'Preview changes without writing files')
37
+ .option('-v, --verbose', 'Enable verbose output')
38
+ .option('-q, --quiet', 'Suppress non-error output')
39
+ .action(async (packages: string[], options: GlobalOptions) => {
40
+ await runWatch(packages, options)
41
+ })
42
+
43
+ cli
44
+ .command('validate [...packages]', 'Check documentation freshness and validate MDX syntax')
45
+ .option('-r, --root <dir>', 'Root directory of the monorepo', {default: process.cwd()})
46
+ .option('-v, --verbose', 'Enable verbose output')
47
+ .option('-q, --quiet', 'Suppress non-error output')
48
+ .action(async (packages: string[], options: GlobalOptions) => {
49
+ await runValidate(packages, options)
50
+ })
51
+
52
+ cli
53
+ .command('[...packages]', 'Sync documentation (default command)')
54
+ .option('-r, --root <dir>', 'Root directory of the monorepo', {default: process.cwd()})
55
+ .option('-w, --watch', 'Watch for changes and sync automatically')
56
+ .option('-d, --dry-run', 'Preview changes without writing files')
57
+ .option('-v, --verbose', 'Enable verbose output')
58
+ .option('-q, --quiet', 'Suppress non-error output')
59
+ .option('-i, --interactive', 'Use interactive package selection')
60
+ .action(async (packages: string[], options: GlobalOptions & {watch?: boolean}) => {
61
+ if (options.watch === true) {
62
+ await runWatch(packages, options)
63
+ } else {
64
+ await runSync(packages, options)
65
+ }
66
+ })
67
+
68
+ cli.help()
69
+ cli.version('0.0.1')
70
+
71
+ cli.parse()
@@ -0,0 +1,19 @@
1
+ export interface GlobalOptions {
2
+ readonly root: string
3
+ readonly dryRun?: boolean
4
+ readonly verbose?: boolean
5
+ readonly quiet?: boolean
6
+ readonly interactive?: boolean
7
+ }
8
+
9
+ export interface PackageSelectionOption {
10
+ readonly value: string
11
+ readonly label: string
12
+ readonly hint?: string
13
+ }
14
+
15
+ export interface ValidationStatus {
16
+ readonly packageName: string
17
+ readonly isValid: boolean
18
+ readonly issues: readonly string[]
19
+ }
package/src/cli/ui.ts ADDED
@@ -0,0 +1,123 @@
1
+ import type {GlobalOptions, PackageSelectionOption} from './types.js'
2
+
3
+ import * as p from '@clack/prompts'
4
+ import {consola} from 'consola'
5
+
6
+ export interface Logger {
7
+ readonly info: (message: string) => void
8
+ readonly success: (message: string) => void
9
+ readonly warn: (message: string) => void
10
+ readonly error: (message: string) => void
11
+ readonly debug: (message: string) => void
12
+ }
13
+
14
+ export interface LoggerOptions {
15
+ readonly verbose?: boolean
16
+ readonly quiet?: boolean
17
+ }
18
+
19
+ export function createLogger(options: LoggerOptions): Logger {
20
+ const {verbose = false, quiet = false} = options
21
+
22
+ return {
23
+ info(message: string): void {
24
+ if (!quiet) {
25
+ p.log.info(message)
26
+ }
27
+ },
28
+ success(message: string): void {
29
+ if (!quiet) {
30
+ p.log.success(message)
31
+ }
32
+ },
33
+ warn(message: string): void {
34
+ p.log.warn(message)
35
+ },
36
+ error(message: string): void {
37
+ p.log.error(message)
38
+ },
39
+ debug(message: string): void {
40
+ if (verbose) {
41
+ consola.debug(message)
42
+ }
43
+ },
44
+ }
45
+ }
46
+
47
+ export function showIntro(title: string): void {
48
+ p.intro(title)
49
+ }
50
+
51
+ export function showOutro(message: string): void {
52
+ p.outro(message)
53
+ }
54
+
55
+ export function createSpinner(): ReturnType<typeof p.spinner> {
56
+ return p.spinner()
57
+ }
58
+
59
+ export async function selectPackages(
60
+ availablePackages: readonly PackageSelectionOption[],
61
+ ): Promise<readonly string[] | symbol> {
62
+ if (availablePackages.length === 0) {
63
+ return []
64
+ }
65
+
66
+ const options = availablePackages.map(pkg => ({
67
+ value: pkg.value,
68
+ label: pkg.label,
69
+ hint: pkg.hint,
70
+ }))
71
+
72
+ const selected = await p.multiselect({
73
+ message: 'Select packages to sync',
74
+ options,
75
+ required: false,
76
+ })
77
+
78
+ return selected
79
+ }
80
+
81
+ export async function confirmAction(message: string): Promise<boolean | symbol> {
82
+ return p.confirm({message})
83
+ }
84
+
85
+ export function handleCancel(value: unknown): value is symbol {
86
+ return p.isCancel(value)
87
+ }
88
+
89
+ export function showCancel(message = 'Operation cancelled.'): void {
90
+ p.cancel(message)
91
+ }
92
+
93
+ export function formatDuration(ms: number): string {
94
+ if (ms < 1000) {
95
+ return `${ms}ms`
96
+ }
97
+ if (ms < 60000) {
98
+ return `${(ms / 1000).toFixed(1)}s`
99
+ }
100
+ const minutes = Math.floor(ms / 60000)
101
+ const seconds = Math.round((ms % 60000) / 1000)
102
+ return `${minutes}m ${seconds}s`
103
+ }
104
+
105
+ export function formatPackageList(packages: readonly string[]): string {
106
+ if (packages.length === 0) {
107
+ return 'no packages'
108
+ }
109
+ if (packages.length === 1) {
110
+ return packages[0] ?? 'unknown'
111
+ }
112
+ if (packages.length <= 3) {
113
+ return packages.join(', ')
114
+ }
115
+ return `${packages.slice(0, 3).join(', ')} and ${packages.length - 3} more`
116
+ }
117
+
118
+ export function createProgressCallback(options: GlobalOptions): (message: string) => void {
119
+ const logger = createLogger(options)
120
+ return (message: string) => {
121
+ logger.debug(message)
122
+ }
123
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @bfra.me/doc-sync/generators/api-reference-generator - API documentation table generation
3
+ */
4
+
5
+ import type {ExportedFunction, ExportedType, PackageAPI} from '../types'
6
+
7
+ export function generateAPIReference(api: PackageAPI): string {
8
+ const sections: string[] = []
9
+
10
+ if (api.functions.length > 0) {
11
+ sections.push('### Functions')
12
+ sections.push('')
13
+ sections.push(generateFunctionsTable(api.functions))
14
+ sections.push('')
15
+ sections.push(generateFunctionDetails(api.functions))
16
+ }
17
+
18
+ if (api.types.length > 0) {
19
+ sections.push('### Types')
20
+ sections.push('')
21
+ sections.push(generateTypesTable(api.types))
22
+ sections.push('')
23
+ sections.push(generateTypeDetails(api.types))
24
+ }
25
+
26
+ return sections.join('\n')
27
+ }
28
+
29
+ function generateFunctionsTable(functions: readonly ExportedFunction[]): string {
30
+ const lines: string[] = []
31
+
32
+ lines.push('| Function | Description |')
33
+ lines.push('| -------- | ----------- |')
34
+
35
+ for (const fn of functions) {
36
+ const name = formatFunctionName(fn)
37
+ const description = fn.jsdoc?.description ?? ''
38
+ const firstLine = getFirstLine(description)
39
+ lines.push(`| ${name} | ${escapeTableCell(firstLine)} |`)
40
+ }
41
+
42
+ return lines.join('\n')
43
+ }
44
+
45
+ function generateFunctionDetails(functions: readonly ExportedFunction[]): string {
46
+ const sections: string[] = []
47
+
48
+ for (const fn of functions) {
49
+ sections.push(generateFunctionDetail(fn))
50
+ }
51
+
52
+ return sections.join('\n\n')
53
+ }
54
+
55
+ function generateFunctionDetail(fn: ExportedFunction): string {
56
+ const lines: string[] = []
57
+
58
+ lines.push(`#### \`${fn.name}\``)
59
+ lines.push('')
60
+
61
+ if (fn.jsdoc?.description !== undefined) {
62
+ lines.push(fn.jsdoc.description)
63
+ lines.push('')
64
+ }
65
+
66
+ lines.push('```typescript')
67
+ lines.push(fn.signature)
68
+ lines.push('```')
69
+
70
+ if (fn.parameters.length > 0) {
71
+ lines.push('')
72
+ lines.push('**Parameters:**')
73
+ lines.push('')
74
+ lines.push('| Name | Type | Description |')
75
+ lines.push('| ---- | ---- | ----------- |')
76
+
77
+ for (const param of fn.parameters) {
78
+ const paramDoc = fn.jsdoc?.params?.find(p => p.name === param.name)
79
+ const description = paramDoc?.description ?? ''
80
+ const optional = param.optional ? ' (optional)' : ''
81
+ lines.push(
82
+ `| \`${param.name}\`${optional} | \`${escapeCode(param.type)}\` | ${escapeTableCell(description)} |`,
83
+ )
84
+ }
85
+ }
86
+
87
+ if (fn.returnType !== 'void') {
88
+ lines.push('')
89
+ lines.push(`**Returns:** \`${escapeCode(fn.returnType)}\``)
90
+
91
+ if (fn.jsdoc?.returns !== undefined) {
92
+ lines.push('')
93
+ lines.push(fn.jsdoc.returns)
94
+ }
95
+ }
96
+
97
+ if (fn.jsdoc?.deprecated !== undefined) {
98
+ lines.push('')
99
+ lines.push(`:::caution[Deprecated]`)
100
+ lines.push(fn.jsdoc.deprecated)
101
+ lines.push(':::')
102
+ }
103
+
104
+ if (fn.jsdoc?.since !== undefined) {
105
+ lines.push('')
106
+ lines.push(`*Since: ${fn.jsdoc.since}*`)
107
+ }
108
+
109
+ return lines.join('\n')
110
+ }
111
+
112
+ function generateTypesTable(types: readonly ExportedType[]): string {
113
+ const lines: string[] = []
114
+
115
+ lines.push('| Type | Kind | Description |')
116
+ lines.push('| ---- | ---- | ----------- |')
117
+
118
+ for (const type of types) {
119
+ const name = `[\`${type.name}\`](#${type.name.toLowerCase()})`
120
+ const kind = type.kind
121
+ const description = type.jsdoc?.description ?? ''
122
+ const firstLine = getFirstLine(description)
123
+ lines.push(`| ${name} | ${kind} | ${escapeTableCell(firstLine)} |`)
124
+ }
125
+
126
+ return lines.join('\n')
127
+ }
128
+
129
+ function generateTypeDetails(types: readonly ExportedType[]): string {
130
+ const sections: string[] = []
131
+
132
+ for (const type of types) {
133
+ sections.push(generateTypeDetail(type))
134
+ }
135
+
136
+ return sections.join('\n\n')
137
+ }
138
+
139
+ function generateTypeDetail(type: ExportedType): string {
140
+ const lines: string[] = []
141
+
142
+ lines.push(`#### \`${type.name}\``)
143
+ lines.push('')
144
+
145
+ if (type.jsdoc?.description !== undefined) {
146
+ lines.push(type.jsdoc.description)
147
+ lines.push('')
148
+ }
149
+
150
+ lines.push('```typescript')
151
+ lines.push(type.definition)
152
+ lines.push('```')
153
+
154
+ if (type.typeParameters !== undefined && type.typeParameters.length > 0) {
155
+ lines.push('')
156
+ lines.push(`**Type Parameters:** ${type.typeParameters.map(t => `\`${t}\``).join(', ')}`)
157
+ }
158
+
159
+ if (type.jsdoc?.deprecated !== undefined) {
160
+ lines.push('')
161
+ lines.push(`:::caution[Deprecated]`)
162
+ lines.push(type.jsdoc.deprecated)
163
+ lines.push(':::')
164
+ }
165
+
166
+ if (type.jsdoc?.since !== undefined) {
167
+ lines.push('')
168
+ lines.push(`*Since: ${type.jsdoc.since}*`)
169
+ }
170
+
171
+ return lines.join('\n')
172
+ }
173
+
174
+ function formatFunctionName(fn: ExportedFunction): string {
175
+ const name = `[\`${fn.name}\`](#${fn.name.toLowerCase()})`
176
+ const badges: string[] = []
177
+
178
+ if (fn.isAsync) {
179
+ badges.push('<Badge text="async" variant="note" size="small" />')
180
+ }
181
+
182
+ if (fn.isGenerator) {
183
+ badges.push('<Badge text="generator" variant="tip" size="small" />')
184
+ }
185
+
186
+ if (fn.isDefault) {
187
+ badges.push('<Badge text="default" variant="caution" size="small" />')
188
+ }
189
+
190
+ return badges.length > 0 ? `${name} ${badges.join(' ')}` : name
191
+ }
192
+
193
+ function getFirstLine(text: string): string {
194
+ const firstLine = text.split('\n')[0]?.trim() ?? ''
195
+
196
+ if (firstLine.length > 100) {
197
+ const truncated = firstLine.slice(0, 97)
198
+ const lastSpace = truncated.lastIndexOf(' ')
199
+ if (lastSpace > 50) {
200
+ return `${truncated.slice(0, lastSpace)}...`
201
+ }
202
+ return `${truncated}...`
203
+ }
204
+
205
+ return firstLine
206
+ }
207
+
208
+ function escapeTableCell(content: string): string {
209
+ return content.replaceAll('|', String.raw`\|`).replaceAll('\n', ' ')
210
+ }
211
+
212
+ function escapeCode(code: string): string {
213
+ return code.replaceAll('`', '\\`')
214
+ }
215
+
216
+ export function generateAPICompact(api: PackageAPI): string {
217
+ const lines: string[] = []
218
+
219
+ if (api.functions.length > 0) {
220
+ lines.push('**Functions:**')
221
+ for (const fn of api.functions) {
222
+ lines.push(`- \`${fn.name}(${formatParameterList(fn)})\` → \`${fn.returnType}\``)
223
+ }
224
+ }
225
+
226
+ if (api.types.length > 0) {
227
+ if (lines.length > 0) lines.push('')
228
+ lines.push('**Types:**')
229
+ for (const type of api.types) {
230
+ lines.push(`- \`${type.name}\` (${type.kind})`)
231
+ }
232
+ }
233
+
234
+ return lines.join('\n')
235
+ }
236
+
237
+ function formatParameterList(fn: ExportedFunction): string {
238
+ return fn.parameters.map(p => (p.optional ? `${p.name}?` : p.name)).join(', ')
239
+ }
240
+
241
+ export function generateCategoryReference(
242
+ functions: readonly ExportedFunction[],
243
+ types: readonly ExportedType[],
244
+ categoryName: string,
245
+ ): string {
246
+ const sections: string[] = []
247
+
248
+ sections.push(`### ${categoryName}`)
249
+ sections.push('')
250
+
251
+ if (functions.length > 0) {
252
+ sections.push('#### Functions')
253
+ sections.push('')
254
+ sections.push(generateFunctionsTable(functions))
255
+ sections.push('')
256
+ sections.push(generateFunctionDetails(functions))
257
+ }
258
+
259
+ if (types.length > 0) {
260
+ sections.push('#### Types')
261
+ sections.push('')
262
+ sections.push(generateTypesTable(types))
263
+ sections.push('')
264
+ sections.push(generateTypeDetails(types))
265
+ }
266
+
267
+ return sections.join('\n')
268
+ }