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