@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,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bfra.me/doc-sync/parsers/export-analyzer - Public API surface analyzer for package exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {Project, SourceFile} from 'ts-morph'
|
|
6
|
+
|
|
7
|
+
import type {PackageAPI, ParseResult, ReExport} from '../types'
|
|
8
|
+
|
|
9
|
+
import {ok} from '@bfra.me/es/result'
|
|
10
|
+
|
|
11
|
+
import {createProject, extractPackageAPI, parseSourceFile} from './typescript-parser'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for analyzing package exports
|
|
15
|
+
*/
|
|
16
|
+
export interface ExportAnalyzerOptions {
|
|
17
|
+
readonly tsConfigPath?: string
|
|
18
|
+
readonly followReExports?: boolean
|
|
19
|
+
readonly maxDepth?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolved export information including the original source
|
|
24
|
+
*/
|
|
25
|
+
export interface ResolvedExport {
|
|
26
|
+
readonly name: string
|
|
27
|
+
readonly kind: 'function' | 'type' | 'interface' | 'enum' | 'class' | 're-export'
|
|
28
|
+
readonly source: string
|
|
29
|
+
readonly isDefault: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Complete analysis of a package's public API
|
|
34
|
+
*/
|
|
35
|
+
export interface PublicAPIAnalysis {
|
|
36
|
+
readonly packagePath: string
|
|
37
|
+
readonly entryPoint: string
|
|
38
|
+
readonly api: PackageAPI
|
|
39
|
+
readonly resolvedExports: readonly ResolvedExport[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Analyzes the public API surface of a package from its entry point
|
|
44
|
+
*/
|
|
45
|
+
export function analyzePublicAPI(
|
|
46
|
+
entryPointPath: string,
|
|
47
|
+
options?: ExportAnalyzerOptions,
|
|
48
|
+
): ParseResult<PublicAPIAnalysis> {
|
|
49
|
+
const project = createProject({tsConfigPath: options?.tsConfigPath})
|
|
50
|
+
const sourceFileResult = parseSourceFile(project, entryPointPath)
|
|
51
|
+
|
|
52
|
+
if (!sourceFileResult.success) {
|
|
53
|
+
return sourceFileResult
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const sourceFile = sourceFileResult.data
|
|
57
|
+
const api = extractPackageAPI(sourceFile)
|
|
58
|
+
const resolvedExports = resolveAllExports(sourceFile, api, project, options)
|
|
59
|
+
|
|
60
|
+
return ok({
|
|
61
|
+
packagePath: sourceFile.getDirectoryPath(),
|
|
62
|
+
entryPoint: entryPointPath,
|
|
63
|
+
api,
|
|
64
|
+
resolvedExports,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveAllExports(
|
|
69
|
+
sourceFile: SourceFile,
|
|
70
|
+
api: PackageAPI,
|
|
71
|
+
project: Project,
|
|
72
|
+
options?: ExportAnalyzerOptions,
|
|
73
|
+
): readonly ResolvedExport[] {
|
|
74
|
+
const exports: ResolvedExport[] = []
|
|
75
|
+
const filePath = sourceFile.getFilePath()
|
|
76
|
+
|
|
77
|
+
// Add direct function exports
|
|
78
|
+
for (const func of api.functions) {
|
|
79
|
+
exports.push({
|
|
80
|
+
name: func.name,
|
|
81
|
+
kind: 'function',
|
|
82
|
+
source: filePath,
|
|
83
|
+
isDefault: func.isDefault,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add direct type exports
|
|
88
|
+
for (const type of api.types) {
|
|
89
|
+
exports.push({
|
|
90
|
+
name: type.name,
|
|
91
|
+
kind: type.kind,
|
|
92
|
+
source: filePath,
|
|
93
|
+
isDefault: type.isDefault,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle re-exports if enabled
|
|
98
|
+
if (options?.followReExports !== false) {
|
|
99
|
+
const maxDepth = options?.maxDepth ?? 5
|
|
100
|
+
const reExportedItems = resolveReExports(
|
|
101
|
+
sourceFile,
|
|
102
|
+
api.reExports,
|
|
103
|
+
project,
|
|
104
|
+
maxDepth,
|
|
105
|
+
new Set(),
|
|
106
|
+
)
|
|
107
|
+
exports.push(...reExportedItems)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return exports
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveReExports(
|
|
114
|
+
sourceFile: SourceFile,
|
|
115
|
+
reExports: readonly ReExport[],
|
|
116
|
+
project: Project,
|
|
117
|
+
remainingDepth: number,
|
|
118
|
+
visited: Set<string>,
|
|
119
|
+
): readonly ResolvedExport[] {
|
|
120
|
+
if (remainingDepth <= 0 || reExports.length === 0) {
|
|
121
|
+
return []
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const exports: ResolvedExport[] = []
|
|
125
|
+
|
|
126
|
+
for (const reExport of reExports) {
|
|
127
|
+
const resolvedPath = resolveModulePath(sourceFile, reExport.from)
|
|
128
|
+
if (resolvedPath === undefined || visited.has(resolvedPath)) {
|
|
129
|
+
// Add as unresolved re-export
|
|
130
|
+
if (reExport.exports === '*') {
|
|
131
|
+
exports.push({
|
|
132
|
+
name: reExport.alias ?? '*',
|
|
133
|
+
kind: 're-export',
|
|
134
|
+
source: reExport.from,
|
|
135
|
+
isDefault: false,
|
|
136
|
+
})
|
|
137
|
+
} else {
|
|
138
|
+
for (const name of reExport.exports) {
|
|
139
|
+
exports.push({
|
|
140
|
+
name,
|
|
141
|
+
kind: 're-export',
|
|
142
|
+
source: reExport.from,
|
|
143
|
+
isDefault: false,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
visited.add(resolvedPath)
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const reExportedFile = getOrAddSourceFile(project, resolvedPath)
|
|
154
|
+
if (reExportedFile === undefined) continue
|
|
155
|
+
|
|
156
|
+
const reExportedAPI = extractPackageAPI(reExportedFile)
|
|
157
|
+
|
|
158
|
+
if (reExport.exports === '*') {
|
|
159
|
+
// Namespace export - include all exports from the module
|
|
160
|
+
for (const func of reExportedAPI.functions) {
|
|
161
|
+
exports.push({
|
|
162
|
+
name: func.name,
|
|
163
|
+
kind: 'function',
|
|
164
|
+
source: resolvedPath,
|
|
165
|
+
isDefault: func.isDefault,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const type of reExportedAPI.types) {
|
|
170
|
+
exports.push({
|
|
171
|
+
name: type.name,
|
|
172
|
+
kind: type.kind,
|
|
173
|
+
source: resolvedPath,
|
|
174
|
+
isDefault: type.isDefault,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Recurse into nested re-exports
|
|
179
|
+
const nestedExports = resolveReExports(
|
|
180
|
+
reExportedFile,
|
|
181
|
+
reExportedAPI.reExports,
|
|
182
|
+
project,
|
|
183
|
+
remainingDepth - 1,
|
|
184
|
+
visited,
|
|
185
|
+
)
|
|
186
|
+
exports.push(...nestedExports)
|
|
187
|
+
} else {
|
|
188
|
+
// Named exports - only include specified exports
|
|
189
|
+
for (const exportName of reExport.exports) {
|
|
190
|
+
// Parse potential alias: "original as alias"
|
|
191
|
+
const [originalName, alias] = parseExportName(exportName)
|
|
192
|
+
|
|
193
|
+
// Check if this is a function export
|
|
194
|
+
const func = reExportedAPI.functions.find(f => f.name === originalName)
|
|
195
|
+
if (func !== undefined) {
|
|
196
|
+
exports.push({
|
|
197
|
+
name: alias ?? originalName,
|
|
198
|
+
kind: 'function',
|
|
199
|
+
source: resolvedPath,
|
|
200
|
+
isDefault: func.isDefault,
|
|
201
|
+
})
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if this is a type export
|
|
206
|
+
const type = reExportedAPI.types.find(t => t.name === originalName)
|
|
207
|
+
if (type !== undefined) {
|
|
208
|
+
exports.push({
|
|
209
|
+
name: alias ?? originalName,
|
|
210
|
+
kind: type.kind,
|
|
211
|
+
source: resolvedPath,
|
|
212
|
+
isDefault: type.isDefault,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// Failed to resolve re-export, skip
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return exports
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function parseExportName(exportName: string): [string, string | undefined] {
|
|
226
|
+
const asIndex = exportName.indexOf(' as ')
|
|
227
|
+
if (asIndex > 0) {
|
|
228
|
+
return [exportName.slice(0, asIndex), exportName.slice(asIndex + 4)]
|
|
229
|
+
}
|
|
230
|
+
return [exportName, undefined]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resolveModulePath(sourceFile: SourceFile, modulePath: string): string | undefined {
|
|
234
|
+
if (!modulePath.startsWith('.')) {
|
|
235
|
+
// External module, cannot resolve
|
|
236
|
+
return undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const directory = sourceFile.getDirectory()
|
|
241
|
+
const extensions = ['.ts', '.tsx', '/index.ts', '/index.tsx', '.js', '.jsx']
|
|
242
|
+
|
|
243
|
+
for (const ext of extensions) {
|
|
244
|
+
const candidate = directory.getSourceFile(modulePath + ext)
|
|
245
|
+
if (candidate !== undefined) {
|
|
246
|
+
return candidate.getFilePath()
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Try without extension
|
|
251
|
+
const direct = directory.getSourceFile(modulePath)
|
|
252
|
+
return direct?.getFilePath()
|
|
253
|
+
} catch {
|
|
254
|
+
return undefined
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getOrAddSourceFile(project: Project, filePath: string): SourceFile | undefined {
|
|
259
|
+
try {
|
|
260
|
+
return project.getSourceFile(filePath) ?? project.addSourceFileAtPath(filePath)
|
|
261
|
+
} catch {
|
|
262
|
+
return undefined
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Finds all exported symbols from a package entry point
|
|
268
|
+
*/
|
|
269
|
+
export function findExportedSymbols(
|
|
270
|
+
entryPointPath: string,
|
|
271
|
+
options?: ExportAnalyzerOptions,
|
|
272
|
+
): ParseResult<readonly string[]> {
|
|
273
|
+
const analysisResult = analyzePublicAPI(entryPointPath, options)
|
|
274
|
+
|
|
275
|
+
if (!analysisResult.success) {
|
|
276
|
+
return analysisResult
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const symbols = analysisResult.data.resolvedExports.map(exp => exp.name)
|
|
280
|
+
return ok([...new Set(symbols)])
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Checks if a symbol is exported from a package
|
|
285
|
+
*/
|
|
286
|
+
export function isSymbolExported(
|
|
287
|
+
entryPointPath: string,
|
|
288
|
+
symbolName: string,
|
|
289
|
+
options?: ExportAnalyzerOptions,
|
|
290
|
+
): ParseResult<boolean> {
|
|
291
|
+
const analysisResult = analyzePublicAPI(entryPointPath, options)
|
|
292
|
+
|
|
293
|
+
if (!analysisResult.success) {
|
|
294
|
+
return analysisResult
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const isExported = analysisResult.data.resolvedExports.some(exp => exp.name === symbolName)
|
|
298
|
+
return ok(isExported)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Gets detailed information about a specific exported symbol
|
|
303
|
+
*/
|
|
304
|
+
export function getExportedSymbolInfo(
|
|
305
|
+
entryPointPath: string,
|
|
306
|
+
symbolName: string,
|
|
307
|
+
options?: ExportAnalyzerOptions,
|
|
308
|
+
): ParseResult<ResolvedExport | undefined> {
|
|
309
|
+
const analysisResult = analyzePublicAPI(entryPointPath, options)
|
|
310
|
+
|
|
311
|
+
if (!analysisResult.success) {
|
|
312
|
+
return analysisResult
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const exported = analysisResult.data.resolvedExports.find(exp => exp.name === symbolName)
|
|
316
|
+
return ok(exported)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Filters exports by kind
|
|
321
|
+
*/
|
|
322
|
+
export function getExportsByKind(
|
|
323
|
+
entryPointPath: string,
|
|
324
|
+
kind: ResolvedExport['kind'],
|
|
325
|
+
options?: ExportAnalyzerOptions,
|
|
326
|
+
): ParseResult<readonly ResolvedExport[]> {
|
|
327
|
+
const analysisResult = analyzePublicAPI(entryPointPath, options)
|
|
328
|
+
|
|
329
|
+
if (!analysisResult.success) {
|
|
330
|
+
return analysisResult
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const filtered = analysisResult.data.resolvedExports.filter(exp => exp.kind === kind)
|
|
334
|
+
return ok(filtered)
|
|
335
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bfra.me/doc-sync/parsers/guards - Type guards and validation for parser types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
DocConfigSource,
|
|
7
|
+
ExportedFunction,
|
|
8
|
+
ExportedType,
|
|
9
|
+
JSDocInfo,
|
|
10
|
+
JSDocParam,
|
|
11
|
+
JSDocTag,
|
|
12
|
+
MDXFrontmatter,
|
|
13
|
+
PackageAPI,
|
|
14
|
+
PackageInfo,
|
|
15
|
+
ParseError,
|
|
16
|
+
ReadmeContent,
|
|
17
|
+
ReadmeSection,
|
|
18
|
+
ReExport,
|
|
19
|
+
SyncError,
|
|
20
|
+
} from '../types'
|
|
21
|
+
|
|
22
|
+
export function isParseError(value: unknown): value is ParseError {
|
|
23
|
+
if (typeof value !== 'object' || value === null) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const obj = value as Record<string, unknown>
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
typeof obj.code === 'string' &&
|
|
31
|
+
typeof obj.message === 'string' &&
|
|
32
|
+
[
|
|
33
|
+
'INVALID_SYNTAX',
|
|
34
|
+
'FILE_NOT_FOUND',
|
|
35
|
+
'READ_ERROR',
|
|
36
|
+
'MALFORMED_JSON',
|
|
37
|
+
'UNSUPPORTED_FORMAT',
|
|
38
|
+
].includes(obj.code)
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Type guard for SyncError
|
|
44
|
+
*/
|
|
45
|
+
export function isSyncError(value: unknown): value is SyncError {
|
|
46
|
+
if (typeof value !== 'object' || value === null) {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const obj = value as Record<string, unknown>
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
typeof obj.code === 'string' &&
|
|
54
|
+
typeof obj.message === 'string' &&
|
|
55
|
+
[
|
|
56
|
+
'WRITE_ERROR',
|
|
57
|
+
'VALIDATION_ERROR',
|
|
58
|
+
'GENERATION_ERROR',
|
|
59
|
+
'PACKAGE_NOT_FOUND',
|
|
60
|
+
'CONFIG_ERROR',
|
|
61
|
+
].includes(obj.code)
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isJSDocParam(value: unknown): value is JSDocParam {
|
|
66
|
+
if (typeof value !== 'object' || value === null) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const obj = value as Record<string, unknown>
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
typeof obj.name === 'string' &&
|
|
74
|
+
(obj.type === undefined || typeof obj.type === 'string') &&
|
|
75
|
+
(obj.description === undefined || typeof obj.description === 'string') &&
|
|
76
|
+
(obj.optional === undefined || typeof obj.optional === 'boolean') &&
|
|
77
|
+
(obj.defaultValue === undefined || typeof obj.defaultValue === 'string')
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isJSDocTag(value: unknown): value is JSDocTag {
|
|
82
|
+
if (typeof value !== 'object' || value === null) {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const obj = value as Record<string, unknown>
|
|
87
|
+
|
|
88
|
+
return typeof obj.name === 'string' && (obj.value === undefined || typeof obj.value === 'string')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isJSDocInfo(value: unknown): value is JSDocInfo {
|
|
92
|
+
if (typeof value !== 'object' || value === null) {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const obj = value as Record<string, unknown>
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
(obj.description === undefined || typeof obj.description === 'string') &&
|
|
100
|
+
(obj.params === undefined || (Array.isArray(obj.params) && obj.params.every(isJSDocParam))) &&
|
|
101
|
+
(obj.returns === undefined || typeof obj.returns === 'string') &&
|
|
102
|
+
(obj.examples === undefined ||
|
|
103
|
+
(Array.isArray(obj.examples) && obj.examples.every(e => typeof e === 'string'))) &&
|
|
104
|
+
(obj.deprecated === undefined || typeof obj.deprecated === 'string') &&
|
|
105
|
+
(obj.since === undefined || typeof obj.since === 'string') &&
|
|
106
|
+
(obj.see === undefined ||
|
|
107
|
+
(Array.isArray(obj.see) && obj.see.every(s => typeof s === 'string'))) &&
|
|
108
|
+
(obj.customTags === undefined ||
|
|
109
|
+
(Array.isArray(obj.customTags) && obj.customTags.every(isJSDocTag)))
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function isExportedFunction(value: unknown): value is ExportedFunction {
|
|
114
|
+
if (typeof value !== 'object' || value === null) {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const obj = value as Record<string, unknown>
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
typeof obj.name === 'string' &&
|
|
122
|
+
typeof obj.signature === 'string' &&
|
|
123
|
+
typeof obj.isAsync === 'boolean' &&
|
|
124
|
+
typeof obj.isGenerator === 'boolean' &&
|
|
125
|
+
Array.isArray(obj.parameters) &&
|
|
126
|
+
typeof obj.returnType === 'string' &&
|
|
127
|
+
typeof obj.isDefault === 'boolean'
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function isExportedType(value: unknown): value is ExportedType {
|
|
132
|
+
if (typeof value !== 'object' || value === null) {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const obj = value as Record<string, unknown>
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
typeof obj.name === 'string' &&
|
|
140
|
+
typeof obj.definition === 'string' &&
|
|
141
|
+
typeof obj.kind === 'string' &&
|
|
142
|
+
['interface', 'type', 'enum', 'class'].includes(obj.kind) &&
|
|
143
|
+
typeof obj.isDefault === 'boolean'
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isReExport(value: unknown): value is ReExport {
|
|
148
|
+
if (typeof value !== 'object' || value === null) {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const obj = value as Record<string, unknown>
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
typeof obj.from === 'string' &&
|
|
156
|
+
(obj.exports === '*' ||
|
|
157
|
+
(Array.isArray(obj.exports) && obj.exports.every(e => typeof e === 'string')))
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function isPackageAPI(value: unknown): value is PackageAPI {
|
|
162
|
+
if (typeof value !== 'object' || value === null) {
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const obj = value as Record<string, unknown>
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
Array.isArray(obj.functions) &&
|
|
170
|
+
obj.functions.every(isExportedFunction) &&
|
|
171
|
+
Array.isArray(obj.types) &&
|
|
172
|
+
obj.types.every(isExportedType) &&
|
|
173
|
+
Array.isArray(obj.reExports) &&
|
|
174
|
+
obj.reExports.every(isReExport)
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function isDocConfigSource(value: unknown): value is DocConfigSource {
|
|
179
|
+
if (typeof value !== 'object' || value === null) {
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const obj = value as Record<string, unknown>
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
(obj.title === undefined || typeof obj.title === 'string') &&
|
|
187
|
+
(obj.description === undefined || typeof obj.description === 'string') &&
|
|
188
|
+
(obj.sidebar === undefined || typeof obj.sidebar === 'object') &&
|
|
189
|
+
(obj.excludeSections === undefined ||
|
|
190
|
+
(Array.isArray(obj.excludeSections) &&
|
|
191
|
+
obj.excludeSections.every(s => typeof s === 'string'))) &&
|
|
192
|
+
(obj.frontmatter === undefined || typeof obj.frontmatter === 'object')
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function isPackageInfo(value: unknown): value is PackageInfo {
|
|
197
|
+
if (typeof value !== 'object' || value === null) {
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const obj = value as Record<string, unknown>
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
typeof obj.name === 'string' &&
|
|
205
|
+
typeof obj.version === 'string' &&
|
|
206
|
+
typeof obj.packagePath === 'string' &&
|
|
207
|
+
typeof obj.srcPath === 'string' &&
|
|
208
|
+
(obj.description === undefined || typeof obj.description === 'string') &&
|
|
209
|
+
(obj.keywords === undefined ||
|
|
210
|
+
(Array.isArray(obj.keywords) && obj.keywords.every(k => typeof k === 'string'))) &&
|
|
211
|
+
(obj.readmePath === undefined || typeof obj.readmePath === 'string') &&
|
|
212
|
+
(obj.docsConfig === undefined || isDocConfigSource(obj.docsConfig))
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function isReadmeSection(value: unknown): value is ReadmeSection {
|
|
217
|
+
if (typeof value !== 'object' || value === null) {
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const obj = value as Record<string, unknown>
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
typeof obj.heading === 'string' &&
|
|
225
|
+
typeof obj.level === 'number' &&
|
|
226
|
+
obj.level >= 1 &&
|
|
227
|
+
obj.level <= 6 &&
|
|
228
|
+
typeof obj.content === 'string' &&
|
|
229
|
+
Array.isArray(obj.children) &&
|
|
230
|
+
obj.children.every(isReadmeSection)
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function isReadmeContent(value: unknown): value is ReadmeContent {
|
|
235
|
+
if (typeof value !== 'object' || value === null) {
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const obj = value as Record<string, unknown>
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
(obj.title === undefined || typeof obj.title === 'string') &&
|
|
243
|
+
(obj.preamble === undefined || typeof obj.preamble === 'string') &&
|
|
244
|
+
Array.isArray(obj.sections) &&
|
|
245
|
+
obj.sections.every(isReadmeSection) &&
|
|
246
|
+
typeof obj.raw === 'string'
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function isMDXFrontmatter(value: unknown): value is MDXFrontmatter {
|
|
251
|
+
if (typeof value !== 'object' || value === null) {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const obj = value as Record<string, unknown>
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
typeof obj.title === 'string' &&
|
|
259
|
+
(obj.description === undefined || typeof obj.description === 'string') &&
|
|
260
|
+
(obj.sidebar === undefined || typeof obj.sidebar === 'object') &&
|
|
261
|
+
(obj.tableOfContents === undefined ||
|
|
262
|
+
typeof obj.tableOfContents === 'boolean' ||
|
|
263
|
+
typeof obj.tableOfContents === 'object') &&
|
|
264
|
+
(obj.template === undefined || obj.template === 'doc' || obj.template === 'splash')
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function isValidPackageName(name: string): boolean {
|
|
269
|
+
// Package names must not be empty
|
|
270
|
+
if (name.length === 0) {
|
|
271
|
+
return false
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Package names must not start with a dot or underscore
|
|
275
|
+
if (name.startsWith('.') || name.startsWith('_')) {
|
|
276
|
+
return false
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Scoped package names
|
|
280
|
+
if (name.startsWith('@')) {
|
|
281
|
+
const slashIndex = name.indexOf('/')
|
|
282
|
+
if (slashIndex <= 1 || slashIndex === name.length - 1) {
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const scope = name.slice(1, slashIndex)
|
|
287
|
+
const pkg = name.slice(slashIndex + 1)
|
|
288
|
+
|
|
289
|
+
return isValidUnscopedPackageName(scope) && isValidUnscopedPackageName(pkg)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return isValidUnscopedPackageName(name)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isValidUnscopedPackageName(name: string): boolean {
|
|
296
|
+
return /^\w[\w.-]*$/.test(name)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function isValidSemver(version: string): boolean {
|
|
300
|
+
return /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(version)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function isValidHeadingLevel(level: number): level is 1 | 2 | 3 | 4 | 5 | 6 {
|
|
304
|
+
return Number.isInteger(level) && level >= 1 && level <= 6
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Checks for potential XSS patterns to prevent injection attacks */
|
|
308
|
+
export function isSafeContent(content: string): boolean {
|
|
309
|
+
const dangerousPatterns = [/<script\b/i, /javascript:/i, /on\w+\s*=/i, /data:/i, /vbscript:/i]
|
|
310
|
+
|
|
311
|
+
return !dangerousPatterns.some(pattern => pattern.test(content))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Prevents directory traversal attacks in file paths */
|
|
315
|
+
export function isSafeFilePath(filePath: string): boolean {
|
|
316
|
+
const normalizedPath = filePath
|
|
317
|
+
.replaceAll('\\', '/')
|
|
318
|
+
.replaceAll(/\/+/g, '/')
|
|
319
|
+
.replaceAll('/./', '/')
|
|
320
|
+
|
|
321
|
+
// Check for directory traversal
|
|
322
|
+
if (normalizedPath.includes('../') || normalizedPath.includes('..\\')) {
|
|
323
|
+
return false
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check for absolute paths on Unix
|
|
327
|
+
if (normalizedPath.startsWith('/') && !filePath.startsWith('/')) {
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return true
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function assertParseError(value: unknown): asserts value is ParseError {
|
|
335
|
+
if (!isParseError(value)) {
|
|
336
|
+
throw new TypeError('Expected ParseError')
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function assertPackageInfo(value: unknown): asserts value is PackageInfo {
|
|
341
|
+
if (!isPackageInfo(value)) {
|
|
342
|
+
throw new TypeError('Expected PackageInfo')
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function assertPackageAPI(value: unknown): asserts value is PackageAPI {
|
|
347
|
+
if (!isPackageAPI(value)) {
|
|
348
|
+
throw new TypeError('Expected PackageAPI')
|
|
349
|
+
}
|
|
350
|
+
}
|