@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,1027 @@
1
+ import {
2
+ createHeadingPattern,
3
+ extractCodeBlocks,
4
+ findEmptyMarkdownLinks,
5
+ generateMDXDocument,
6
+ hasComponent,
7
+ mergeContent,
8
+ validateMDXSyntax
9
+ } from "./chunk-G5KKGJYO.js";
10
+ import {
11
+ analyzePublicAPI,
12
+ parsePackageComplete,
13
+ parseReadmeFile
14
+ } from "./chunk-6NKAJT2M.js";
15
+
16
+ // src/orchestrator/package-scanner.ts
17
+ import fs from "fs/promises";
18
+ import path from "path";
19
+ import { err, ok } from "@bfra.me/es/result";
20
+ var DEFAULT_OPTIONS = {
21
+ includePatterns: ["packages/*"],
22
+ parseSourceFiles: true,
23
+ parseReadme: true
24
+ };
25
+ function createPackageScanner(options) {
26
+ const {
27
+ rootDir,
28
+ includePatterns = DEFAULT_OPTIONS.includePatterns,
29
+ excludePackages = [],
30
+ parseSourceFiles = DEFAULT_OPTIONS.parseSourceFiles,
31
+ parseReadme = DEFAULT_OPTIONS.parseReadme
32
+ } = options;
33
+ const docsOutputDir = path.join(rootDir, "docs", "src", "content", "docs", "packages");
34
+ async function discoverPackages() {
35
+ const packagePaths = [];
36
+ for (const pattern of includePatterns) {
37
+ const baseDir = path.join(rootDir, pattern.replace("/*", ""));
38
+ try {
39
+ const entries = await fs.readdir(baseDir, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ if (!entry.isDirectory()) {
42
+ continue;
43
+ }
44
+ const packagePath = path.join(baseDir, entry.name);
45
+ const packageJsonPath = path.join(packagePath, "package.json");
46
+ try {
47
+ await fs.access(packageJsonPath);
48
+ packagePaths.push(packagePath);
49
+ } catch {
50
+ }
51
+ }
52
+ } catch {
53
+ }
54
+ }
55
+ return packagePaths;
56
+ }
57
+ async function findSourceFiles(srcDir) {
58
+ const sourceFiles = [];
59
+ try {
60
+ await collectSourceFiles(srcDir, sourceFiles);
61
+ } catch {
62
+ }
63
+ return sourceFiles;
64
+ }
65
+ async function collectSourceFiles(dir, files) {
66
+ const entries = await fs.readdir(dir, { withFileTypes: true });
67
+ for (const entry of entries) {
68
+ const fullPath = path.join(dir, entry.name);
69
+ if (entry.isDirectory()) {
70
+ if (entry.name === "__tests__" || entry.name === "__mocks__") {
71
+ continue;
72
+ }
73
+ await collectSourceFiles(fullPath, files);
74
+ } else if (entry.isFile()) {
75
+ const ext = path.extname(entry.name).toLowerCase();
76
+ if ((ext === ".ts" || ext === ".tsx") && !entry.name.includes(".test.") && !entry.name.includes(".spec.")) {
77
+ files.push(fullPath);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ async function scanPackage(packagePath) {
83
+ const packageResult = await parsePackageComplete(packagePath);
84
+ if (!packageResult.success) {
85
+ return err({
86
+ code: "PACKAGE_NOT_FOUND",
87
+ message: `Failed to parse package at ${packagePath}: ${packageResult.error.message}`,
88
+ filePath: packagePath,
89
+ cause: packageResult.error
90
+ });
91
+ }
92
+ const info = packageResult.data;
93
+ const sourceFiles = await findSourceFiles(info.srcPath);
94
+ let readme;
95
+ if (parseReadme && info.readmePath !== void 0) {
96
+ const readmeResult = await parseReadmeFile(info.readmePath);
97
+ if (readmeResult.success) {
98
+ readme = readmeResult.data;
99
+ }
100
+ }
101
+ let api;
102
+ if (parseSourceFiles && sourceFiles.length > 0) {
103
+ const entryFile = findEntryFile(sourceFiles, info.srcPath);
104
+ if (entryFile !== void 0) {
105
+ const analysisResult = analyzePublicAPI(entryFile);
106
+ if (analysisResult.success) {
107
+ api = analysisResult.data.api;
108
+ }
109
+ }
110
+ }
111
+ const docSlug = buildDocSlug(info.name);
112
+ const existingDocPath = path.join(docsOutputDir, `${docSlug}.mdx`);
113
+ let hasExistingDoc = false;
114
+ try {
115
+ await fs.access(existingDocPath);
116
+ hasExistingDoc = true;
117
+ } catch {
118
+ }
119
+ return ok({
120
+ info,
121
+ readme,
122
+ api,
123
+ sourceFiles,
124
+ needsDocumentation: true,
125
+ existingDocPath: hasExistingDoc ? existingDocPath : void 0
126
+ });
127
+ }
128
+ return {
129
+ async scan() {
130
+ const startTime = Date.now();
131
+ const packagePaths = await discoverPackages();
132
+ const packages = [];
133
+ const errors = [];
134
+ for (const packagePath of packagePaths) {
135
+ const result = await scanPackage(packagePath);
136
+ if (result.success) {
137
+ const scanned = result.data;
138
+ if (excludePackages.includes(scanned.info.name)) {
139
+ continue;
140
+ }
141
+ packages.push(scanned);
142
+ } else {
143
+ errors.push(result.error);
144
+ }
145
+ }
146
+ const packagesNeedingDocs = packages.filter((pkg) => pkg.needsDocumentation);
147
+ return {
148
+ packages,
149
+ packagesNeedingDocs,
150
+ errors,
151
+ durationMs: Date.now() - startTime
152
+ };
153
+ },
154
+ scanPackage
155
+ };
156
+ }
157
+ function findEntryFile(sourceFiles, srcDir) {
158
+ const indexPath = path.join(srcDir, "index.ts");
159
+ return sourceFiles.find((file) => file === indexPath) ?? sourceFiles[0];
160
+ }
161
+ function buildDocSlug(packageName) {
162
+ return getUnscopedName(packageName).toLowerCase().replaceAll(/[^a-z0-9-]/g, "-");
163
+ }
164
+ function getUnscopedName(packageName) {
165
+ if (packageName.startsWith("@")) {
166
+ const slashIndex = packageName.indexOf("/");
167
+ if (slashIndex > 0) {
168
+ return packageName.slice(slashIndex + 1);
169
+ }
170
+ }
171
+ return packageName;
172
+ }
173
+ function filterPackagesByPattern(packages, pattern) {
174
+ const regex = new RegExp(pattern.replaceAll("*", ".*"), "i");
175
+ return packages.filter((pkg) => regex.test(pkg.info.name));
176
+ }
177
+ function groupPackagesByScope(packages) {
178
+ const grouped = /* @__PURE__ */ new Map();
179
+ for (const pkg of packages) {
180
+ const scope = getPackageScope(pkg.info.name) ?? "__unscoped__";
181
+ const existing = grouped.get(scope);
182
+ if (existing === void 0) {
183
+ grouped.set(scope, [pkg]);
184
+ } else {
185
+ existing.push(pkg);
186
+ }
187
+ }
188
+ return grouped;
189
+ }
190
+ function getPackageScope(packageName) {
191
+ if (packageName.startsWith("@")) {
192
+ const slashIndex = packageName.indexOf("/");
193
+ if (slashIndex > 0) {
194
+ return packageName.slice(0, slashIndex);
195
+ }
196
+ }
197
+ return void 0;
198
+ }
199
+
200
+ // src/watcher/file-watcher.ts
201
+ import path2 from "path";
202
+ import process from "process";
203
+ import { createFileWatcher as createBaseWatcher } from "@bfra.me/es/watcher";
204
+ var DEFAULT_WATCH_PATTERNS = [
205
+ "packages/*/README.md",
206
+ "packages/*/readme.md",
207
+ "packages/*/package.json",
208
+ "packages/*/src/**/*.ts",
209
+ "packages/*/src/**/*.tsx"
210
+ ];
211
+ var DEFAULT_IGNORE_PATTERNS = [
212
+ "**/node_modules/**",
213
+ "**/lib/**",
214
+ "**/dist/**",
215
+ "**/.git/**",
216
+ "**/coverage/**",
217
+ "**/*.test.ts",
218
+ "**/*.spec.ts",
219
+ "**/__tests__/**",
220
+ "**/__mocks__/**"
221
+ ];
222
+ function extractPackageName(filePath, root) {
223
+ const relativePath = path2.relative(root, filePath);
224
+ const parts = relativePath.split(path2.sep);
225
+ if (parts[0] === "packages" && parts.length > 1) {
226
+ return parts[1];
227
+ }
228
+ return void 0;
229
+ }
230
+ function createDocWatcher(options = {}) {
231
+ const {
232
+ rootDir = process.cwd(),
233
+ debounceMs = 300,
234
+ additionalIgnore = [],
235
+ usePolling = false
236
+ } = options;
237
+ const watchPaths = DEFAULT_WATCH_PATTERNS.map((pattern) => path2.join(rootDir, pattern));
238
+ const ignoredPatterns = [...DEFAULT_IGNORE_PATTERNS, ...additionalIgnore];
239
+ const watcherOptions = {
240
+ debounceMs,
241
+ ignored: ignoredPatterns,
242
+ usePolling
243
+ };
244
+ const baseWatcher = createBaseWatcher(watchPaths, watcherOptions);
245
+ const handlers = /* @__PURE__ */ new Set();
246
+ function transformToDocEvents(changes) {
247
+ return changes.map((change) => ({
248
+ type: change.type,
249
+ path: change.path,
250
+ packageName: extractPackageName(change.path, rootDir),
251
+ timestamp: new Date(change.timestamp)
252
+ }));
253
+ }
254
+ return {
255
+ async start() {
256
+ baseWatcher.on("change", (event) => {
257
+ const docEvents = transformToDocEvents(event.changes);
258
+ for (const handler of handlers) {
259
+ Promise.resolve(handler(docEvents)).catch((error) => {
260
+ console.error("[doc-sync] Error in change handler:", error);
261
+ });
262
+ }
263
+ });
264
+ await baseWatcher.start();
265
+ },
266
+ async close() {
267
+ await baseWatcher.close();
268
+ handlers.clear();
269
+ },
270
+ onChanges(handler) {
271
+ handlers.add(handler);
272
+ return () => {
273
+ handlers.delete(handler);
274
+ };
275
+ },
276
+ getWatchedPaths() {
277
+ return watchPaths;
278
+ }
279
+ };
280
+ }
281
+ function categorizeFile(filePath) {
282
+ const basename = path2.basename(filePath).toLowerCase();
283
+ if (basename === "readme.md" || basename === "readme") {
284
+ return "readme";
285
+ }
286
+ if (basename === "package.json") {
287
+ return "package-json";
288
+ }
289
+ const ext = path2.extname(filePath).toLowerCase();
290
+ if (ext === ".ts" || ext === ".tsx") {
291
+ return "source";
292
+ }
293
+ return "unknown";
294
+ }
295
+ function groupChangesByPackage(events) {
296
+ const grouped = /* @__PURE__ */ new Map();
297
+ for (const event of events) {
298
+ const pkg = event.packageName ?? "__unknown__";
299
+ const existing = grouped.get(pkg);
300
+ if (existing === void 0) {
301
+ grouped.set(pkg, [event]);
302
+ } else {
303
+ existing.push(event);
304
+ }
305
+ }
306
+ return grouped;
307
+ }
308
+ function filterDocumentationChanges(events) {
309
+ return events.filter((event) => {
310
+ const category = categorizeFile(event.path);
311
+ return category !== "unknown";
312
+ });
313
+ }
314
+
315
+ // src/watcher/change-detector.ts
316
+ import { createChangeDetector as createBaseDetector } from "@bfra.me/es/watcher";
317
+ function createDocChangeDetector(options = {}) {
318
+ const baseDetector = createBaseDetector({ algorithm: options.algorithm });
319
+ const packageFiles = /* @__PURE__ */ new Map();
320
+ return {
321
+ async hasChanged(filePath) {
322
+ return baseDetector.hasChanged(filePath);
323
+ },
324
+ async record(filePath) {
325
+ await baseDetector.record(filePath);
326
+ },
327
+ async recordPackage(pkg, files) {
328
+ const fileSet = new Set(files);
329
+ packageFiles.set(pkg.name, fileSet);
330
+ await Promise.all(files.map(async (file) => baseDetector.record(file)));
331
+ },
332
+ clear(filePath) {
333
+ baseDetector.clear(filePath);
334
+ for (const fileSet of packageFiles.values()) {
335
+ fileSet.delete(filePath);
336
+ }
337
+ },
338
+ clearAll() {
339
+ baseDetector.clearAll();
340
+ packageFiles.clear();
341
+ },
342
+ async analyzeChanges(events) {
343
+ const packageChanges = /* @__PURE__ */ new Map();
344
+ for (const event of events) {
345
+ const packageName = event.packageName ?? "__unknown__";
346
+ const category = categorizeFile(event.path);
347
+ let analysis = packageChanges.get(packageName);
348
+ if (analysis === void 0) {
349
+ analysis = { categories: /* @__PURE__ */ new Set(), files: [] };
350
+ packageChanges.set(packageName, analysis);
351
+ }
352
+ if (category !== "unknown") {
353
+ analysis.categories.add(category);
354
+ }
355
+ analysis.files.push(event.path);
356
+ }
357
+ const results = [];
358
+ for (const [packageName, analysis] of packageChanges) {
359
+ const changedCategories = [...analysis.categories];
360
+ const needsRegeneration = changedCategories.includes("readme") || changedCategories.includes("package-json") || changedCategories.includes("source");
361
+ results.push({
362
+ packageName,
363
+ needsRegeneration,
364
+ changedCategories,
365
+ changedFiles: analysis.files
366
+ });
367
+ }
368
+ return results;
369
+ }
370
+ };
371
+ }
372
+ function determineRegenerationScope(changedCategories) {
373
+ const hasReadme = changedCategories.includes("readme");
374
+ const hasSource = changedCategories.includes("source");
375
+ const hasPackageJson = changedCategories.includes("package-json");
376
+ if (hasReadme && hasSource) {
377
+ return "full";
378
+ }
379
+ if (hasSource) {
380
+ return "api-only";
381
+ }
382
+ if (hasReadme) {
383
+ return "readme-only";
384
+ }
385
+ if (hasPackageJson) {
386
+ return "metadata-only";
387
+ }
388
+ return "none";
389
+ }
390
+ async function hasAnyFileChanged(detector, files) {
391
+ const results = await Promise.all(files.map(async (file) => detector.hasChanged(file)));
392
+ return results.some((changed) => changed);
393
+ }
394
+
395
+ // src/watcher/debouncer.ts
396
+ import { createDebouncer as createBaseDebouncer } from "@bfra.me/es/watcher";
397
+ function createDocDebouncer(handler, options = {}) {
398
+ const { debounceMs = 300, maxWaitMs = 5e3 } = options;
399
+ let pendingEvents = [];
400
+ let maxWaitTimeout;
401
+ function processEvents(events) {
402
+ if (maxWaitTimeout !== void 0) {
403
+ clearTimeout(maxWaitTimeout);
404
+ maxWaitTimeout = void 0;
405
+ }
406
+ const deduplicated = deduplicateEvents(events);
407
+ if (deduplicated.length > 0) {
408
+ Promise.resolve(handler(deduplicated)).catch((error) => {
409
+ console.error("[doc-sync] Error in batch handler:", error);
410
+ });
411
+ }
412
+ }
413
+ const baseDebouncer = createBaseDebouncer((events) => {
414
+ processEvents(events);
415
+ pendingEvents = [];
416
+ }, debounceMs);
417
+ function startMaxWaitTimer() {
418
+ if (maxWaitTimeout === void 0) {
419
+ maxWaitTimeout = setTimeout(() => {
420
+ baseDebouncer.flush();
421
+ }, maxWaitMs);
422
+ }
423
+ }
424
+ return {
425
+ add(event) {
426
+ pendingEvents.push(event);
427
+ startMaxWaitTimer();
428
+ baseDebouncer.add(event);
429
+ },
430
+ addAll(events) {
431
+ for (const event of events) {
432
+ pendingEvents.push(event);
433
+ baseDebouncer.add(event);
434
+ }
435
+ if (events.length > 0) {
436
+ startMaxWaitTimer();
437
+ }
438
+ },
439
+ flush() {
440
+ if (maxWaitTimeout !== void 0) {
441
+ clearTimeout(maxWaitTimeout);
442
+ maxWaitTimeout = void 0;
443
+ }
444
+ baseDebouncer.flush();
445
+ },
446
+ cancel() {
447
+ if (maxWaitTimeout !== void 0) {
448
+ clearTimeout(maxWaitTimeout);
449
+ maxWaitTimeout = void 0;
450
+ }
451
+ pendingEvents = [];
452
+ baseDebouncer.cancel();
453
+ },
454
+ getPendingCount() {
455
+ return pendingEvents.length;
456
+ }
457
+ };
458
+ }
459
+ function deduplicateEvents(events) {
460
+ const latestByPath = /* @__PURE__ */ new Map();
461
+ for (const event of events) {
462
+ const existing = latestByPath.get(event.path);
463
+ if (existing === void 0 || event.timestamp > existing.timestamp) {
464
+ latestByPath.set(event.path, event);
465
+ }
466
+ }
467
+ return [...latestByPath.values()];
468
+ }
469
+ function consolidateEvents(events) {
470
+ const byPath = /* @__PURE__ */ new Map();
471
+ for (const event of events) {
472
+ const existing = byPath.get(event.path);
473
+ if (existing === void 0) {
474
+ byPath.set(event.path, [event]);
475
+ } else {
476
+ existing.push(event);
477
+ }
478
+ }
479
+ const consolidated = [];
480
+ for (const [, pathEvents] of byPath) {
481
+ const firstEvent = pathEvents[0];
482
+ if (firstEvent === void 0) {
483
+ continue;
484
+ }
485
+ if (pathEvents.length === 1) {
486
+ consolidated.push(firstEvent);
487
+ continue;
488
+ }
489
+ pathEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
490
+ const first = pathEvents[0];
491
+ const last = pathEvents.at(-1);
492
+ if (first === void 0 || last === void 0) {
493
+ continue;
494
+ }
495
+ if (first.type === "add" && last.type === "unlink") {
496
+ continue;
497
+ }
498
+ if (first.type === "unlink" && last.type === "add") {
499
+ consolidated.push({
500
+ type: "change",
501
+ path: last.path,
502
+ packageName: last.packageName,
503
+ timestamp: last.timestamp
504
+ });
505
+ continue;
506
+ }
507
+ consolidated.push(last);
508
+ }
509
+ return consolidated;
510
+ }
511
+
512
+ // src/orchestrator/validation-pipeline.ts
513
+ import { err as err2, ok as ok2 } from "@bfra.me/es/result";
514
+ var DEFAULT_OPTIONS2 = {
515
+ validateFrontmatter: true,
516
+ validateComponents: true,
517
+ validateContent: true,
518
+ strict: false
519
+ };
520
+ function createValidationPipeline(options = {}) {
521
+ const mergedOptions = { ...DEFAULT_OPTIONS2, ...options };
522
+ function validate(doc) {
523
+ const errors = [];
524
+ const warnings = [];
525
+ if (mergedOptions.validateFrontmatter) {
526
+ const frontmatterResult = validateFrontmatter(doc.frontmatter);
527
+ errors.push(...frontmatterResult.errors);
528
+ warnings.push(...frontmatterResult.warnings);
529
+ }
530
+ const syntaxResult = validateSyntax(doc.rendered);
531
+ errors.push(...syntaxResult.errors);
532
+ warnings.push(...syntaxResult.warnings);
533
+ if (mergedOptions.validateComponents) {
534
+ const componentResult = validateStarlightComponents(doc.rendered);
535
+ errors.push(...componentResult.errors);
536
+ warnings.push(...componentResult.warnings);
537
+ }
538
+ if (mergedOptions.validateContent) {
539
+ const contentResult = validateContentQuality(doc.rendered);
540
+ errors.push(...contentResult.errors);
541
+ warnings.push(...contentResult.warnings);
542
+ }
543
+ const valid = mergedOptions.strict ? errors.length === 0 && warnings.length === 0 : errors.length === 0;
544
+ return { valid, errors, warnings };
545
+ }
546
+ function validateContent(content) {
547
+ const errors = [];
548
+ const warnings = [];
549
+ const syntaxResult = validateSyntax(content);
550
+ errors.push(...syntaxResult.errors);
551
+ warnings.push(...syntaxResult.warnings);
552
+ if (mergedOptions.validateComponents) {
553
+ const componentResult = validateStarlightComponents(content);
554
+ errors.push(...componentResult.errors);
555
+ warnings.push(...componentResult.warnings);
556
+ }
557
+ const valid = mergedOptions.strict ? errors.length === 0 && warnings.length === 0 : errors.length === 0;
558
+ return { valid, errors, warnings };
559
+ }
560
+ function validateMultiple(docs) {
561
+ const results = /* @__PURE__ */ new Map();
562
+ const seen = /* @__PURE__ */ new Set();
563
+ for (const doc of docs) {
564
+ const key = doc.frontmatter.title;
565
+ if (seen.has(key)) {
566
+ return err2({
567
+ code: "VALIDATION_ERROR",
568
+ message: `Duplicate document title detected: "${key}". Each document must have a unique title.`
569
+ });
570
+ }
571
+ seen.add(key);
572
+ results.set(key, validate(doc));
573
+ }
574
+ return ok2(results);
575
+ }
576
+ return { validate, validateContent, validateMultiple };
577
+ }
578
+ function validateFrontmatter(frontmatter) {
579
+ const errors = [];
580
+ const warnings = [];
581
+ if (frontmatter.title.trim().length === 0) {
582
+ errors.push({
583
+ type: "frontmatter",
584
+ message: "Frontmatter title is required and cannot be empty"
585
+ });
586
+ }
587
+ if (frontmatter.title.length > 100) {
588
+ warnings.push({
589
+ type: "recommendation",
590
+ message: `Title is ${frontmatter.title.length} characters, consider keeping under 100 for better readability`
591
+ });
592
+ }
593
+ if (frontmatter.description !== void 0 && frontmatter.description.length > 200) {
594
+ warnings.push({
595
+ type: "recommendation",
596
+ message: "Description is quite long, consider keeping under 200 characters for SEO"
597
+ });
598
+ }
599
+ return { errors, warnings };
600
+ }
601
+ function validateSyntax(content) {
602
+ const errors = [];
603
+ const warnings = [];
604
+ const result = validateMDXSyntax(content);
605
+ if (!result.success) {
606
+ errors.push({
607
+ type: "syntax",
608
+ message: result.error.message
609
+ });
610
+ }
611
+ return { errors, warnings };
612
+ }
613
+ var STARLIGHT_COMPONENTS = ["Badge", "Card", "CardGrid", "Tabs", "TabItem"];
614
+ function validateStarlightComponents(content) {
615
+ const errors = [];
616
+ const warnings = [];
617
+ for (const component of STARLIGHT_COMPONENTS) {
618
+ const openPattern = new RegExp(String.raw`<${component}(?:\s[^>]*)?>`, "g");
619
+ const closePattern = new RegExp(`</${component}>`, "g");
620
+ const selfClosePattern = new RegExp(String.raw`<${component}(?:\s[^>]*)?/>`, "g");
621
+ const opens = content.match(openPattern)?.length ?? 0;
622
+ const closes = content.match(closePattern)?.length ?? 0;
623
+ const selfCloses = content.match(selfClosePattern)?.length ?? 0;
624
+ const nonSelfClosingOpens = opens - selfCloses;
625
+ if (nonSelfClosingOpens > closes) {
626
+ errors.push({
627
+ type: "component",
628
+ message: `Unclosed <${component}> tag detected (${nonSelfClosingOpens} opens, ${closes} closes)`
629
+ });
630
+ }
631
+ }
632
+ const hasTabItem = content.includes("<TabItem");
633
+ const hasTabs = content.includes("<Tabs");
634
+ if (hasTabItem && !hasTabs) {
635
+ errors.push({
636
+ type: "component",
637
+ message: "<TabItem> must be used inside <Tabs>"
638
+ });
639
+ }
640
+ const hasCard = hasComponent(content, "Card");
641
+ const hasCardGrid = hasComponent(content, "CardGrid");
642
+ if (hasCard && !hasCardGrid) {
643
+ warnings.push({
644
+ type: "recommendation",
645
+ message: "Consider wrapping <Card> components in <CardGrid> for better layout"
646
+ });
647
+ }
648
+ return { errors, warnings };
649
+ }
650
+ function validateContentQuality(content) {
651
+ const errors = [];
652
+ const warnings = [];
653
+ const emptyLinkPositions = findEmptyMarkdownLinks(content);
654
+ if (emptyLinkPositions.length > 0) {
655
+ errors.push({
656
+ type: "content",
657
+ message: `Found ${emptyLinkPositions.length} empty link(s)`
658
+ });
659
+ }
660
+ const codeBlockMarkers = content.match(/```/g)?.length ?? 0;
661
+ if (codeBlockMarkers % 2 !== 0) {
662
+ errors.push({
663
+ type: "content",
664
+ message: "Unclosed code block detected (odd number of ``` markers)"
665
+ });
666
+ }
667
+ const h2Pattern = createHeadingPattern(2);
668
+ const h2Headings = [];
669
+ const h2Matches = content.matchAll(h2Pattern);
670
+ for (const match of h2Matches) {
671
+ const heading = match[1];
672
+ if (heading === void 0) {
673
+ continue;
674
+ }
675
+ if (h2Headings.includes(heading)) {
676
+ warnings.push({
677
+ type: "recommendation",
678
+ message: `Duplicate H2 heading: "${heading}"`
679
+ });
680
+ }
681
+ h2Headings.push(heading);
682
+ }
683
+ const codeBlocks = extractCodeBlocks(content);
684
+ for (const block of codeBlocks) {
685
+ const lines = block.split("\n");
686
+ for (const line of lines) {
687
+ if (line.length > 120 && !line.startsWith("```")) {
688
+ warnings.push({
689
+ type: "recommendation",
690
+ message: `Code line exceeds 120 characters, may require horizontal scrolling`
691
+ });
692
+ break;
693
+ }
694
+ }
695
+ }
696
+ return { errors, warnings };
697
+ }
698
+ function validateDocument(doc, options) {
699
+ const pipeline = createValidationPipeline(options);
700
+ const result = pipeline.validate(doc);
701
+ if (!result.valid) {
702
+ const errorMessages = result.errors.map((e) => e.message).join("; ");
703
+ return err2({
704
+ code: "VALIDATION_ERROR",
705
+ message: `MDX validation failed: ${errorMessages}`
706
+ });
707
+ }
708
+ return ok2(doc);
709
+ }
710
+ function validateContentString(content, options) {
711
+ const pipeline = createValidationPipeline(options);
712
+ const result = pipeline.validateContent(content);
713
+ if (!result.valid) {
714
+ const errorMessages = result.errors.map((e) => e.message).join("; ");
715
+ return err2({
716
+ code: "VALIDATION_ERROR",
717
+ message: `MDX validation failed: ${errorMessages}`
718
+ });
719
+ }
720
+ return ok2(content);
721
+ }
722
+
723
+ // src/orchestrator/sync-orchestrator.ts
724
+ import fs2 from "fs/promises";
725
+ import path3 from "path";
726
+ import { err as err3, ok as ok3 } from "@bfra.me/es/result";
727
+ function createSyncOrchestrator(options) {
728
+ const { config, dryRun = false, verbose = false, onProgress, onError } = options;
729
+ const scanner = createPackageScanner({
730
+ rootDir: config.rootDir,
731
+ includePatterns: config.includePatterns,
732
+ excludePackages: config.excludePatterns
733
+ });
734
+ const validationPipeline = createValidationPipeline();
735
+ let watcher;
736
+ let debouncer;
737
+ let watching = false;
738
+ function log(message) {
739
+ if (verbose && onProgress != null) {
740
+ onProgress(message);
741
+ }
742
+ }
743
+ function reportError(error) {
744
+ if (onError != null) {
745
+ onError(error);
746
+ }
747
+ }
748
+ async function syncPackage(pkg) {
749
+ log(`Syncing documentation for ${pkg.info.name}...`);
750
+ const docResult = generateMDXDocument(pkg.info, pkg.readme, pkg.api);
751
+ if (!docResult.success) {
752
+ return docResult;
753
+ }
754
+ const doc = docResult.data;
755
+ const validationResult = validationPipeline.validate(doc);
756
+ if (!validationResult.valid) {
757
+ const errorMessages = validationResult.errors.map((e) => e.message).join("; ");
758
+ return err3({
759
+ code: "VALIDATION_ERROR",
760
+ message: `Validation failed for ${pkg.info.name}: ${errorMessages}`,
761
+ packageName: pkg.info.name
762
+ });
763
+ }
764
+ const outputPath = getOutputPath(pkg.info.name, config.outputDir);
765
+ let action = "created";
766
+ let contentToWrite = doc.rendered;
767
+ try {
768
+ if (pkg.existingDocPath == null) {
769
+ if (!dryRun) {
770
+ await writeFile(outputPath, contentToWrite);
771
+ }
772
+ } else {
773
+ try {
774
+ const existingContent = await fs2.readFile(pkg.existingDocPath, "utf-8");
775
+ const mergedResult = mergeContent(existingContent, doc.rendered);
776
+ if (!mergedResult.success) {
777
+ if (!dryRun) {
778
+ await writeFile(outputPath, contentToWrite);
779
+ }
780
+ action = "updated";
781
+ } else if (mergedResult.data.content === existingContent) {
782
+ return ok3({
783
+ packageName: pkg.info.name,
784
+ outputPath,
785
+ action: "unchanged",
786
+ timestamp: /* @__PURE__ */ new Date()
787
+ });
788
+ } else {
789
+ contentToWrite = mergedResult.data.content;
790
+ action = "updated";
791
+ if (!dryRun) {
792
+ await writeFile(outputPath, contentToWrite);
793
+ }
794
+ }
795
+ } catch {
796
+ if (!dryRun) {
797
+ await writeFile(outputPath, contentToWrite);
798
+ }
799
+ }
800
+ }
801
+ } catch (error) {
802
+ return err3(createWriteError(error, outputPath, pkg.info.name));
803
+ }
804
+ log(`${dryRun ? "[DRY RUN] Would write" : "Wrote"} ${outputPath}`);
805
+ return ok3({
806
+ packageName: pkg.info.name,
807
+ outputPath,
808
+ action,
809
+ timestamp: /* @__PURE__ */ new Date()
810
+ });
811
+ }
812
+ async function syncAll() {
813
+ const startTime = Date.now();
814
+ log("Starting full documentation sync...");
815
+ const scanResult = await scanner.scan();
816
+ const details = [];
817
+ const errors = [...scanResult.errors];
818
+ for (const pkg of scanResult.packagesNeedingDocs) {
819
+ const result = await syncPackage(pkg);
820
+ if (result.success) {
821
+ details.push(result.data);
822
+ } else {
823
+ errors.push(result.error);
824
+ reportError(result.error);
825
+ }
826
+ }
827
+ const successCount = details.filter((d) => d.action !== "unchanged").length;
828
+ const unchangedCount = details.filter((d) => d.action === "unchanged").length;
829
+ log(
830
+ `Sync complete: ${successCount} updated, ${unchangedCount} unchanged, ${errors.length} errors`
831
+ );
832
+ return {
833
+ totalPackages: scanResult.packages.length,
834
+ successCount,
835
+ failureCount: errors.length,
836
+ unchangedCount,
837
+ details,
838
+ errors,
839
+ durationMs: Date.now() - startTime
840
+ };
841
+ }
842
+ async function syncPackages(packageNames) {
843
+ const startTime = Date.now();
844
+ log(`Syncing specific packages: ${packageNames.join(", ")}...`);
845
+ const scanResult = await scanner.scan();
846
+ const packagesToSync = scanResult.packages.filter((pkg) => packageNames.includes(pkg.info.name));
847
+ const details = [];
848
+ const errors = [];
849
+ for (const pkg of packagesToSync) {
850
+ const result = await syncPackage(pkg);
851
+ if (result.success) {
852
+ details.push(result.data);
853
+ } else {
854
+ errors.push(result.error);
855
+ reportError(result.error);
856
+ }
857
+ }
858
+ const successCount = details.filter((d) => d.action !== "unchanged").length;
859
+ const unchangedCount = details.filter((d) => d.action === "unchanged").length;
860
+ return {
861
+ totalPackages: packagesToSync.length,
862
+ successCount,
863
+ failureCount: errors.length,
864
+ unchangedCount,
865
+ details,
866
+ errors,
867
+ durationMs: Date.now() - startTime
868
+ };
869
+ }
870
+ async function handleChanges(events) {
871
+ const startTime = Date.now();
872
+ const groupedChanges = groupChangesByPackage(events);
873
+ const packageNames = [];
874
+ for (const [packageName, packageEvents] of groupedChanges) {
875
+ if (packageName === "__unknown__") {
876
+ continue;
877
+ }
878
+ const categories = packageEvents.map((e) => categorizeFileChange(e.path));
879
+ const scope = determineRegenerationScope(categories);
880
+ if (scope !== "none") {
881
+ packageNames.push(packageName);
882
+ log(`Package ${packageName} needs ${scope} regeneration`);
883
+ }
884
+ }
885
+ if (packageNames.length === 0) {
886
+ return {
887
+ totalPackages: 0,
888
+ successCount: 0,
889
+ failureCount: 0,
890
+ unchangedCount: 0,
891
+ details: [],
892
+ errors: [],
893
+ durationMs: Date.now() - startTime
894
+ };
895
+ }
896
+ return syncPackages(packageNames);
897
+ }
898
+ async function startWatching() {
899
+ if (watching) {
900
+ return;
901
+ }
902
+ log("Starting watch mode...");
903
+ watcher = createDocWatcher({
904
+ rootDir: config.rootDir,
905
+ debounceMs: config.debounceMs ?? 300
906
+ });
907
+ debouncer = createDocDebouncer(
908
+ async (events) => {
909
+ const result = await handleChanges(events);
910
+ log(`Watch mode sync: ${result.successCount} updated, ${result.failureCount} errors`);
911
+ },
912
+ { debounceMs: config.debounceMs ?? 300 }
913
+ );
914
+ watcher.onChanges((events) => {
915
+ for (const event of events) {
916
+ debouncer?.add(event);
917
+ }
918
+ });
919
+ await watcher.start();
920
+ watching = true;
921
+ log("Watch mode started");
922
+ }
923
+ async function stopWatching() {
924
+ if (!watching) {
925
+ return;
926
+ }
927
+ log("Stopping watch mode...");
928
+ debouncer?.cancel();
929
+ await watcher?.close();
930
+ watcher = void 0;
931
+ debouncer = void 0;
932
+ watching = false;
933
+ log("Watch mode stopped");
934
+ }
935
+ return {
936
+ syncAll,
937
+ syncPackages,
938
+ handleChanges,
939
+ startWatching,
940
+ stopWatching,
941
+ isWatching: () => watching
942
+ };
943
+ }
944
+ function getOutputPath(packageName, outputDir) {
945
+ const slug = createSlug(getUnscopedName2(packageName));
946
+ return path3.join(outputDir, `${slug}.mdx`);
947
+ }
948
+ function createSlug(name) {
949
+ return name.toLowerCase().replaceAll(/[^a-z0-9-]/g, "-");
950
+ }
951
+ function getUnscopedName2(packageName) {
952
+ if (packageName.startsWith("@")) {
953
+ const slashIndex = packageName.indexOf("/");
954
+ if (slashIndex > 0) {
955
+ return packageName.slice(slashIndex + 1);
956
+ }
957
+ }
958
+ return packageName;
959
+ }
960
+ async function writeFile(filePath, content) {
961
+ const dir = path3.dirname(filePath);
962
+ await fs2.mkdir(dir, { recursive: true });
963
+ await fs2.writeFile(filePath, content, "utf-8");
964
+ }
965
+ function createWriteError(error, filePath, packageName) {
966
+ const message = formatWriteErrorMessage(error, filePath);
967
+ return {
968
+ code: "WRITE_ERROR",
969
+ message,
970
+ packageName,
971
+ filePath,
972
+ cause: error
973
+ };
974
+ }
975
+ function formatWriteErrorMessage(error, filePath) {
976
+ const errorCode = isNodeError(error) ? error.code : void 0;
977
+ switch (errorCode) {
978
+ case "EACCES":
979
+ return `Permission denied writing to ${filePath}`;
980
+ case "ENOENT":
981
+ return `Directory not found for ${filePath}`;
982
+ case "EISDIR":
983
+ return `Cannot write to directory ${filePath}`;
984
+ case void 0:
985
+ default: {
986
+ const errorMessage = error instanceof Error ? error.message : String(error);
987
+ return `Failed to write ${filePath}: ${errorMessage}`;
988
+ }
989
+ }
990
+ }
991
+ function isNodeError(error) {
992
+ return error instanceof Error && "code" in error;
993
+ }
994
+ function categorizeFileChange(filePath) {
995
+ const basename = path3.basename(filePath).toLowerCase();
996
+ if (basename === "readme.md" || basename === "readme") return "readme";
997
+ if (basename === "package.json") return "package-json";
998
+ if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "source";
999
+ return "unknown";
1000
+ }
1001
+ function isValidFilePath(filePath, rootDir) {
1002
+ const resolvedPath = path3.resolve(filePath);
1003
+ const resolvedRoot = path3.resolve(rootDir);
1004
+ return resolvedPath.startsWith(resolvedRoot);
1005
+ }
1006
+
1007
+ export {
1008
+ createPackageScanner,
1009
+ filterPackagesByPattern,
1010
+ groupPackagesByScope,
1011
+ createDocWatcher,
1012
+ categorizeFile,
1013
+ groupChangesByPackage,
1014
+ filterDocumentationChanges,
1015
+ createDocChangeDetector,
1016
+ determineRegenerationScope,
1017
+ hasAnyFileChanged,
1018
+ createDocDebouncer,
1019
+ deduplicateEvents,
1020
+ consolidateEvents,
1021
+ createValidationPipeline,
1022
+ validateDocument,
1023
+ validateContentString,
1024
+ createSyncOrchestrator,
1025
+ isValidFilePath
1026
+ };
1027
+ //# sourceMappingURL=chunk-DR6UG237.js.map