@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.
- package/README.md +288 -0
- package/lib/chunk-6NKAJT2M.js +1233 -0
- package/lib/chunk-DR6UG237.js +1027 -0
- package/lib/chunk-G5KKGJYO.js +1560 -0
- package/lib/chunk-ROLA7SBB.js +12 -0
- package/lib/cli/index.d.ts +1 -0
- package/lib/cli/index.js +397 -0
- package/lib/generators/index.d.ts +170 -0
- package/lib/generators/index.js +76 -0
- package/lib/index.d.ts +141 -0
- package/lib/index.js +118 -0
- package/lib/parsers/index.d.ts +264 -0
- package/lib/parsers/index.js +113 -0
- package/lib/types.d.ts +388 -0
- package/lib/types.js +7 -0
- package/package.json +99 -0
- package/src/cli/commands/index.ts +3 -0
- package/src/cli/commands/sync.ts +146 -0
- package/src/cli/commands/validate.ts +151 -0
- package/src/cli/commands/watch.ts +74 -0
- package/src/cli/index.ts +71 -0
- package/src/cli/types.ts +19 -0
- package/src/cli/ui.ts +123 -0
- package/src/generators/api-reference-generator.ts +268 -0
- package/src/generators/code-example-formatter.ts +313 -0
- package/src/generators/component-mapper.ts +383 -0
- package/src/generators/content-merger.ts +295 -0
- package/src/generators/frontmatter-generator.ts +277 -0
- package/src/generators/index.ts +56 -0
- package/src/generators/mdx-generator.ts +289 -0
- package/src/index.ts +131 -0
- package/src/orchestrator/index.ts +21 -0
- package/src/orchestrator/package-scanner.ts +276 -0
- package/src/orchestrator/sync-orchestrator.ts +382 -0
- package/src/orchestrator/validation-pipeline.ts +328 -0
- package/src/parsers/export-analyzer.ts +335 -0
- package/src/parsers/guards.ts +350 -0
- package/src/parsers/index.ts +82 -0
- package/src/parsers/jsdoc-extractor.ts +313 -0
- package/src/parsers/package-info.ts +267 -0
- package/src/parsers/readme-parser.ts +334 -0
- package/src/parsers/typescript-parser.ts +299 -0
- package/src/types.ts +423 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/safe-patterns.ts +280 -0
- package/src/utils/sanitization.ts +164 -0
- package/src/watcher/change-detector.ts +138 -0
- package/src/watcher/debouncer.ts +168 -0
- package/src/watcher/file-watcher.ts +164 -0
- 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
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -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()
|
package/src/cli/types.ts
ADDED
|
@@ -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
|
+
}
|