@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,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
|
+
}
|