@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,82 @@
1
+ /**
2
+ * @bfra.me/doc-sync/parsers - Unified export of all parser modules
3
+ */
4
+
5
+ // Export analyzer exports
6
+ export {
7
+ analyzePublicAPI,
8
+ findExportedSymbols,
9
+ getExportedSymbolInfo,
10
+ getExportsByKind,
11
+ isSymbolExported,
12
+ } from './export-analyzer'
13
+ export type {ExportAnalyzerOptions, PublicAPIAnalysis, ResolvedExport} from './export-analyzer'
14
+
15
+ // Type guards and validation exports
16
+ export {
17
+ assertPackageAPI,
18
+ assertPackageInfo,
19
+ assertParseError,
20
+ isDocConfigSource,
21
+ isExportedFunction,
22
+ isExportedType,
23
+ isJSDocInfo,
24
+ isJSDocParam,
25
+ isJSDocTag,
26
+ isMDXFrontmatter,
27
+ isPackageAPI,
28
+ isPackageInfo,
29
+ isParseError,
30
+ isReadmeContent,
31
+ isReadmeSection,
32
+ isReExport,
33
+ isSafeContent,
34
+ isSafeFilePath,
35
+ isSyncError,
36
+ isValidHeadingLevel,
37
+ isValidPackageName,
38
+ isValidSemver,
39
+ } from './guards'
40
+
41
+ // JSDoc extractor exports
42
+ export {extractJSDocInfo, hasJSDoc, parseJSDoc} from './jsdoc-extractor'
43
+ export type {JSDocableDeclaration} from './jsdoc-extractor'
44
+
45
+ // Package info exports
46
+ export {
47
+ buildDocSlug,
48
+ extractDocsConfig,
49
+ findEntryPoint,
50
+ findReadmePath,
51
+ getPackageScope,
52
+ getUnscopedName,
53
+ parsePackageComplete,
54
+ parsePackageJson,
55
+ parsePackageJsonContent,
56
+ } from './package-info'
57
+ export type {PackageInfoOptions, PackageJsonSchema} from './package-info'
58
+
59
+ // README parser exports
60
+ export {
61
+ findSection,
62
+ flattenSections,
63
+ getSectionsByLevel,
64
+ getTableOfContents,
65
+ parseReadme,
66
+ parseReadmeFile,
67
+ } from './readme-parser'
68
+ export type {ReadmeParserOptions} from './readme-parser'
69
+
70
+ // TypeScript parser exports
71
+ export {
72
+ analyzeTypeScriptContent,
73
+ analyzeTypeScriptFile,
74
+ createProject,
75
+ extractExportedFunctions,
76
+ extractExportedTypes,
77
+ extractPackageAPI,
78
+ extractReExports,
79
+ parseSourceContent,
80
+ parseSourceFile,
81
+ } from './typescript-parser'
82
+ export type {TypeScriptParserOptions} from './typescript-parser'
@@ -0,0 +1,313 @@
1
+ /**
2
+ * @bfra.me/doc-sync/parsers/jsdoc-extractor - JSDoc annotation extraction from TypeScript AST nodes
3
+ */
4
+
5
+ import type {
6
+ ClassDeclaration,
7
+ EnumDeclaration,
8
+ FunctionDeclaration,
9
+ InterfaceDeclaration,
10
+ JSDoc,
11
+ JSDocableNode,
12
+ Node,
13
+ TypeAliasDeclaration,
14
+ } from 'ts-morph'
15
+
16
+ import type {JSDocInfo, JSDocParam, JSDocTag} from '../types'
17
+
18
+ /**
19
+ * Supported node types for JSDoc extraction
20
+ */
21
+ export type JSDocableDeclaration =
22
+ | ClassDeclaration
23
+ | EnumDeclaration
24
+ | FunctionDeclaration
25
+ | InterfaceDeclaration
26
+ | TypeAliasDeclaration
27
+
28
+ /**
29
+ * Type guard to check if a node has JSDoc
30
+ */
31
+ export function hasJSDoc(node: Node): node is Node & JSDocableNode {
32
+ return 'getJsDocs' in node && typeof node.getJsDocs === 'function'
33
+ }
34
+
35
+ /**
36
+ * Extracts JSDoc information from an AST node
37
+ */
38
+ export function extractJSDocInfo(node: JSDocableDeclaration): JSDocInfo | undefined {
39
+ if (!hasJSDoc(node)) {
40
+ return undefined
41
+ }
42
+
43
+ const jsDocs = node.getJsDocs()
44
+ if (jsDocs.length === 0) {
45
+ return undefined
46
+ }
47
+
48
+ const jsDoc = jsDocs[0]
49
+ if (jsDoc === undefined) {
50
+ return undefined
51
+ }
52
+
53
+ return parseJSDoc(jsDoc)
54
+ }
55
+
56
+ /**
57
+ * Parses a JSDoc node into structured information
58
+ */
59
+ export function parseJSDoc(jsDoc: JSDoc): JSDocInfo {
60
+ const description = jsDoc.getDescription().trim() || undefined
61
+ const tags = jsDoc.getTags()
62
+
63
+ const params = extractParams(tags)
64
+ const returns = extractReturns(tags)
65
+ const examples = extractExamples(tags)
66
+ const deprecated = extractDeprecated(tags)
67
+ const since = extractSince(tags)
68
+ const see = extractSee(tags)
69
+ const customTags = extractCustomTags(tags)
70
+
71
+ return {
72
+ ...(description !== undefined && {description}),
73
+ ...(params.length > 0 && {params}),
74
+ ...(returns !== undefined && {returns}),
75
+ ...(examples.length > 0 && {examples}),
76
+ ...(deprecated !== undefined && {deprecated}),
77
+ ...(since !== undefined && {since}),
78
+ ...(see.length > 0 && {see}),
79
+ ...(customTags.length > 0 && {customTags}),
80
+ }
81
+ }
82
+
83
+ function extractParams(tags: ReturnType<JSDoc['getTags']>): readonly JSDocParam[] {
84
+ const params: JSDocParam[] = []
85
+
86
+ for (const tag of tags) {
87
+ const tagName = tag.getTagName()
88
+ if (tagName !== 'param') continue
89
+
90
+ const text = getTagText(tag)
91
+ if (text === undefined) continue
92
+
93
+ const parsed = parseParamText(text)
94
+ if (parsed !== undefined) {
95
+ params.push(parsed)
96
+ }
97
+ }
98
+
99
+ return params
100
+ }
101
+
102
+ /**
103
+ * Handles multiple @param text formats:
104
+ * - name - description
105
+ * - {type} name - description
106
+ * - name description
107
+ */
108
+ function parseParamText(text: string): JSDocParam | undefined {
109
+ const trimmed = text.trim()
110
+ if (trimmed.length === 0) {
111
+ return undefined
112
+ }
113
+
114
+ let type: string | undefined
115
+ let remaining = trimmed
116
+
117
+ // Extract type if present: {type} ...
118
+ if (remaining.startsWith('{')) {
119
+ const typeEnd = remaining.indexOf('}')
120
+ if (typeEnd > 0) {
121
+ type = remaining.slice(1, typeEnd)
122
+ remaining = remaining.slice(typeEnd + 1).trim()
123
+ }
124
+ }
125
+
126
+ // Split name from description
127
+ const dashIndex = remaining.indexOf(' - ')
128
+ const spaceIndex = remaining.indexOf(' ')
129
+
130
+ let name: string
131
+ let description: string | undefined
132
+
133
+ if (dashIndex > 0) {
134
+ name = remaining.slice(0, dashIndex).trim()
135
+ description = remaining.slice(dashIndex + 3).trim() || undefined
136
+ } else if (spaceIndex > 0) {
137
+ name = remaining.slice(0, spaceIndex).trim()
138
+ description = remaining.slice(spaceIndex + 1).trim() || undefined
139
+ } else {
140
+ name = remaining
141
+ }
142
+
143
+ // Handle optional parameters [name]
144
+ let optional = false
145
+ if (name.startsWith('[') && name.endsWith(']')) {
146
+ optional = true
147
+ name = name.slice(1, -1)
148
+ }
149
+
150
+ // Handle default values [name=value]
151
+ let defaultValue: string | undefined
152
+ const defaultIndex = name.indexOf('=')
153
+ if (defaultIndex > 0) {
154
+ defaultValue = name.slice(defaultIndex + 1)
155
+ name = name.slice(0, defaultIndex)
156
+ optional = true
157
+ }
158
+
159
+ return {
160
+ name,
161
+ ...(type !== undefined && {type}),
162
+ ...(description !== undefined && {description}),
163
+ ...(optional && {optional}),
164
+ ...(defaultValue !== undefined && {defaultValue}),
165
+ }
166
+ }
167
+
168
+ function extractReturns(tags: ReturnType<JSDoc['getTags']>): string | undefined {
169
+ for (const tag of tags) {
170
+ const tagName = tag.getTagName()
171
+ if (tagName !== 'returns' && tagName !== 'return') continue
172
+
173
+ const text = getTagText(tag)
174
+ if (text !== undefined && text.trim().length > 0) {
175
+ // Strip leading type annotation if present
176
+ let result = text.trim()
177
+ if (result.startsWith('{')) {
178
+ const typeEnd = result.indexOf('}')
179
+ if (typeEnd > 0) {
180
+ result = result.slice(typeEnd + 1).trim()
181
+ }
182
+ }
183
+ return result.length > 0 ? result : undefined
184
+ }
185
+ }
186
+
187
+ return undefined
188
+ }
189
+
190
+ function extractExamples(tags: ReturnType<JSDoc['getTags']>): readonly string[] {
191
+ const examples: string[] = []
192
+
193
+ for (const tag of tags) {
194
+ const tagName = tag.getTagName()
195
+ if (tagName !== 'example') continue
196
+
197
+ const text = getTagText(tag)
198
+ if (text !== undefined && text.trim().length > 0) {
199
+ examples.push(text.trim())
200
+ }
201
+ }
202
+
203
+ return examples
204
+ }
205
+
206
+ function extractDeprecated(tags: ReturnType<JSDoc['getTags']>): string | undefined {
207
+ for (const tag of tags) {
208
+ const tagName = tag.getTagName()
209
+ if (tagName !== 'deprecated') continue
210
+
211
+ const text = getTagText(tag)
212
+ return text?.trim() ?? ''
213
+ }
214
+
215
+ return undefined
216
+ }
217
+
218
+ function extractSince(tags: ReturnType<JSDoc['getTags']>): string | undefined {
219
+ for (const tag of tags) {
220
+ const tagName = tag.getTagName()
221
+ if (tagName !== 'since') continue
222
+
223
+ const text = getTagText(tag)
224
+ if (text !== undefined && text.trim().length > 0) {
225
+ return text.trim()
226
+ }
227
+ }
228
+
229
+ return undefined
230
+ }
231
+
232
+ function extractSee(tags: ReturnType<JSDoc['getTags']>): readonly string[] {
233
+ const see: string[] = []
234
+
235
+ for (const tag of tags) {
236
+ const tagName = tag.getTagName()
237
+ if (tagName !== 'see') continue
238
+
239
+ const text = getTagText(tag)
240
+ if (text !== undefined && text.trim().length > 0) {
241
+ see.push(text.trim())
242
+ }
243
+ }
244
+
245
+ return see
246
+ }
247
+
248
+ const STANDARD_TAGS = new Set([
249
+ 'param',
250
+ 'returns',
251
+ 'return',
252
+ 'example',
253
+ 'deprecated',
254
+ 'since',
255
+ 'see',
256
+ 'type',
257
+ 'typedef',
258
+ 'callback',
259
+ 'template',
260
+ 'throws',
261
+ 'async',
262
+ 'generator',
263
+ 'override',
264
+ 'readonly',
265
+ 'private',
266
+ 'protected',
267
+ 'public',
268
+ 'module',
269
+ 'exports',
270
+ 'interface',
271
+ 'enum',
272
+ 'class',
273
+ 'constructor',
274
+ 'function',
275
+ 'method',
276
+ 'property',
277
+ 'member',
278
+ 'implements',
279
+ 'extends',
280
+ 'augments',
281
+ ])
282
+
283
+ function extractCustomTags(tags: ReturnType<JSDoc['getTags']>): readonly JSDocTag[] {
284
+ const customTags: JSDocTag[] = []
285
+
286
+ for (const tag of tags) {
287
+ const tagName = tag.getTagName()
288
+ if (STANDARD_TAGS.has(tagName)) continue
289
+
290
+ const text = getTagText(tag)
291
+ const value = text !== undefined && text.trim().length > 0 ? text.trim() : undefined
292
+
293
+ customTags.push({
294
+ name: tagName,
295
+ ...(value !== undefined && {value}),
296
+ })
297
+ }
298
+
299
+ return customTags
300
+ }
301
+
302
+ function getTagText(tag: ReturnType<JSDoc['getTags']>[number]): string | undefined {
303
+ const fullText = tag.getText()
304
+ const tagName = tag.getTagName()
305
+ const prefix = `@${tagName}`
306
+
307
+ if (fullText.startsWith(prefix)) {
308
+ const text = fullText.slice(prefix.length).trim()
309
+ return text.length > 0 ? text : undefined
310
+ }
311
+
312
+ return undefined
313
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * @bfra.me/doc-sync/parsers/package-info - Package.json metadata extraction
3
+ */
4
+
5
+ import type {DocConfigSource, PackageInfo, ParseError, ParseResult} from '../types'
6
+
7
+ import path from 'node:path'
8
+
9
+ import {err, ok} from '@bfra.me/es/result'
10
+
11
+ /**
12
+ * Schema for validating package.json structure (runtime validation)
13
+ */
14
+ export interface PackageJsonSchema {
15
+ readonly name: string
16
+ readonly version: string
17
+ readonly description?: string
18
+ readonly keywords?: readonly string[]
19
+ readonly main?: string
20
+ readonly module?: string
21
+ readonly types?: string
22
+ readonly exports?: Record<string, unknown>
23
+ readonly repository?:
24
+ | string
25
+ | {
26
+ readonly type?: string
27
+ readonly url?: string
28
+ readonly directory?: string
29
+ }
30
+ readonly docs?: DocConfigSource
31
+ }
32
+
33
+ /**
34
+ * Options for parsing package.json files
35
+ */
36
+ export interface PackageInfoOptions {
37
+ readonly validateSchema?: boolean
38
+ readonly extractDocsConfig?: boolean
39
+ }
40
+
41
+ /**
42
+ * Parses a package.json file and extracts relevant metadata
43
+ */
44
+ export async function parsePackageJson(
45
+ packagePath: string,
46
+ options?: PackageInfoOptions,
47
+ ): Promise<ParseResult<PackageInfo>> {
48
+ const packageJsonPath = packagePath.endsWith('package.json')
49
+ ? packagePath
50
+ : path.join(packagePath, 'package.json')
51
+
52
+ try {
53
+ const fs = await import('node:fs/promises')
54
+ const content = await fs.readFile(packageJsonPath, 'utf-8')
55
+ return parsePackageJsonContent(content, packageJsonPath, options)
56
+ } catch (error) {
57
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
58
+ return err({
59
+ code: 'FILE_NOT_FOUND',
60
+ message: `package.json not found: ${packageJsonPath}`,
61
+ filePath: packageJsonPath,
62
+ cause: error,
63
+ } satisfies ParseError)
64
+ }
65
+
66
+ return err({
67
+ code: 'READ_ERROR',
68
+ message: `Failed to read package.json: ${packageJsonPath}`,
69
+ filePath: packageJsonPath,
70
+ cause: error,
71
+ } satisfies ParseError)
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Parses package.json content from a string
77
+ */
78
+ export function parsePackageJsonContent(
79
+ content: string,
80
+ filePath: string,
81
+ options?: PackageInfoOptions,
82
+ ): ParseResult<PackageInfo> {
83
+ let parsed: unknown
84
+
85
+ try {
86
+ parsed = JSON.parse(content)
87
+ } catch (error) {
88
+ return err({
89
+ code: 'MALFORMED_JSON',
90
+ message: `Invalid JSON in package.json: ${filePath}`,
91
+ filePath,
92
+ cause: error,
93
+ } satisfies ParseError)
94
+ }
95
+
96
+ if (!isPackageJson(parsed)) {
97
+ return err({
98
+ code: 'INVALID_SYNTAX',
99
+ message: 'package.json is missing required fields (name, version)',
100
+ filePath,
101
+ } satisfies ParseError)
102
+ }
103
+
104
+ const packageDir = path.dirname(filePath)
105
+
106
+ return ok({
107
+ name: parsed.name,
108
+ version: parsed.version,
109
+ ...(parsed.description !== undefined && {description: parsed.description}),
110
+ ...(parsed.keywords !== undefined && {keywords: parsed.keywords}),
111
+ packagePath: packageDir,
112
+ srcPath: path.join(packageDir, 'src'),
113
+ ...(options?.extractDocsConfig !== false &&
114
+ parsed.docs !== undefined && {docsConfig: parsed.docs}),
115
+ })
116
+ }
117
+
118
+ function isPackageJson(value: unknown): value is PackageJsonSchema {
119
+ if (typeof value !== 'object' || value === null) {
120
+ return false
121
+ }
122
+
123
+ const obj = value as Record<string, unknown>
124
+
125
+ if (typeof obj.name !== 'string' || obj.name.length === 0) {
126
+ return false
127
+ }
128
+
129
+ if (typeof obj.version !== 'string' || obj.version.length === 0) {
130
+ return false
131
+ }
132
+
133
+ return true
134
+ }
135
+
136
+ /**
137
+ * Extracts documentation configuration from package.json
138
+ */
139
+ export function extractDocsConfig(pkg: PackageInfo): DocConfigSource | undefined {
140
+ return pkg.docsConfig
141
+ }
142
+
143
+ /**
144
+ * Checks if a README file exists for a package
145
+ */
146
+ export async function findReadmePath(packagePath: string): Promise<string | undefined> {
147
+ const fs = await import('node:fs/promises')
148
+ const candidates = ['README.md', 'readme.md', 'Readme.md', 'README.MD', 'README']
149
+
150
+ for (const candidate of candidates) {
151
+ const readmePath = path.join(packagePath, candidate)
152
+ try {
153
+ await fs.access(readmePath)
154
+ return readmePath
155
+ } catch {
156
+ // Continue to next candidate
157
+ }
158
+ }
159
+
160
+ return undefined
161
+ }
162
+
163
+ /**
164
+ * Extracts the source entry point from package.json
165
+ */
166
+ export function findEntryPoint(pkg: PackageInfo, content: string): string {
167
+ try {
168
+ const parsed = JSON.parse(content) as PackageJsonSchema
169
+
170
+ // Check exports field first
171
+ if (parsed.exports !== undefined) {
172
+ const mainExport = parsed.exports['.']
173
+ if (mainExport !== undefined && typeof mainExport === 'object') {
174
+ const exportObj = mainExport as Record<string, unknown>
175
+ if (typeof exportObj.source === 'string') {
176
+ return path.join(pkg.packagePath, exportObj.source)
177
+ }
178
+ if (typeof exportObj.import === 'string') {
179
+ // Convert lib/ to src/ for source analysis
180
+ const importPath = exportObj.import
181
+ if (importPath.includes('/lib/')) {
182
+ return path.join(
183
+ pkg.packagePath,
184
+ importPath.replace('/lib/', '/src/').replace('.js', '.ts'),
185
+ )
186
+ }
187
+ return path.join(pkg.packagePath, importPath)
188
+ }
189
+ }
190
+ if (typeof mainExport === 'string') {
191
+ return path.join(pkg.packagePath, mainExport)
192
+ }
193
+ }
194
+
195
+ // Fall back to types or main field
196
+ if (typeof parsed.types === 'string') {
197
+ const typesPath = parsed.types.replace('/lib/', '/src/').replace('.d.ts', '.ts')
198
+ return path.join(pkg.packagePath, typesPath)
199
+ }
200
+
201
+ if (typeof parsed.main === 'string') {
202
+ const mainPath = parsed.main.replace('/lib/', '/src/').replace('.js', '.ts')
203
+ return path.join(pkg.packagePath, mainPath)
204
+ }
205
+ } catch {
206
+ // Ignore parsing errors
207
+ }
208
+
209
+ // Default to src/index.ts
210
+ return path.join(pkg.srcPath, 'index.ts')
211
+ }
212
+
213
+ /**
214
+ * Parses a complete package including README detection
215
+ */
216
+ export async function parsePackageComplete(
217
+ packagePath: string,
218
+ options?: PackageInfoOptions,
219
+ ): Promise<ParseResult<PackageInfo>> {
220
+ const packageInfoResult = await parsePackageJson(packagePath, options)
221
+
222
+ if (!packageInfoResult.success) {
223
+ return packageInfoResult
224
+ }
225
+
226
+ const readmePath = await findReadmePath(packageInfoResult.data.packagePath)
227
+
228
+ return ok({
229
+ ...packageInfoResult.data,
230
+ ...(readmePath !== undefined && {readmePath}),
231
+ })
232
+ }
233
+
234
+ /**
235
+ * Gets the package scope from a scoped package name
236
+ */
237
+ export function getPackageScope(packageName: string): string | undefined {
238
+ if (packageName.startsWith('@')) {
239
+ const slashIndex = packageName.indexOf('/')
240
+ if (slashIndex > 0) {
241
+ return packageName.slice(0, slashIndex)
242
+ }
243
+ }
244
+ return undefined
245
+ }
246
+
247
+ /**
248
+ * Gets the unscoped package name
249
+ */
250
+ export function getUnscopedName(packageName: string): string {
251
+ if (packageName.startsWith('@')) {
252
+ const slashIndex = packageName.indexOf('/')
253
+ if (slashIndex > 0) {
254
+ return packageName.slice(slashIndex + 1)
255
+ }
256
+ }
257
+ return packageName
258
+ }
259
+
260
+ /**
261
+ * Builds a documentation slug from a package name
262
+ */
263
+ export function buildDocSlug(packageName: string): string {
264
+ return getUnscopedName(packageName)
265
+ .toLowerCase()
266
+ .replaceAll(/[^a-z0-9-]/g, '-')
267
+ }